"""Плоская форма ОСВ 1С (#59, change osv-flat-form): стратегия `header_account` и счёт шапки у строк.""" import pytest from gridbuilder import L, M, sheet_from_rows from src.contract.result import OsvDocument from src.parser.deadline import Deadline from src.parser.osv import OsvParser from src.parser.rules import Rules, load_rules TIMEOUT_SECONDS = 60 HEADER_ACCOUNT_CODE = "OSV_HIERARCHY_BY_HEADER_ACCOUNT" @pytest.fixture(scope="module") def rules() -> Rules: return load_rules() def _table_head() -> list[list[object]]: return [ ["Субконто", M("Сальдо на начало периода", 2), M("Обороты за период", 2), M("Сальдо на конец периода", 2)], [None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"], ] def _parse(rules: Rules, rows: list[list[object]]) -> tuple[OsvDocument, list[str]]: result = OsvParser(rules).parse_sheet(sheet_from_rows(rows), Deadline(TIMEOUT_SECONDS)) return result.document, [note.code for note in result.notes] def test_flat_list_s40(rules: Rules) -> None: """Образец s40: счёт «по счету: 76.Н.1», вид «Контрагенты», организация ниже периода, данные без оформления.""" rows: list[list[object]] = [ [], ["Оборотно-сальдовая ведомость по счету: 76.Н.1"], ["Контрагенты"], ["за 2025 г."], ['ООО "Товарное хозяйство"'], [], ["Единица измерения: рубль (код по ОКЕИ 383)"], [], *_table_head(), ["Август - Агро ООО", 1000, None, 500, 300, 1200, None], ["АВИС ООО", None, 200, 100, 400, None, 500], ["Агроаспект ООО", None, None, 50, 50, None, None], ["Итого развернутое", 1000, 200, 650, 750, 1200, 500], ["Итого", 800, None, 650, 750, 700, None], ] document, codes = _parse(rules, rows) assert document.status == "done" assert HEADER_ACCOUNT_CODE in codes data = [item for item in document.rows if item.group_type != "TOTAL"] assert [item.row_value for item in data] == ["Август - Агро ООО", "АВИС ООО", "Агроаспект ООО"] assert all(item.level == 0 and item.parent_row_number is None for item in data) assert {item.account_code_raw for item in data} == {"76.Н.1"} assert document.header.organization_name == 'ООО "Товарное хозяйство"' def test_flat_list_s41(rules: Rules) -> None: """Образец s41 (R-10): схема [ACCOUNT, COUNTERPARTY («Организации»)], счёт шапки «60».""" rows: list[list[object]] = [ [], ["Оборотно-сальдовая ведомость по счету: 60"], ["Организации"], ["за 2 Квартал 2026 г. План счетов ООО Хреновская Нива"], [], *_table_head(), ["Автодор-Платные Дороги ООО", 820, None, 5000, 4991, 829, None], ["АГРОПРОФИ ООО", None, 2764, None, None, None, 2764], ["Итого развернутое", 820, 2764, 5000, 4991, 829, 2764], ["Итого", None, 1944, 5000, 4991, None, 1935], ] document, _ = _parse(rules, rows) assert document.status == "done" data = [item for item in document.rows if item.group_type != "TOTAL"] assert [item.account_code_raw for item in data] == ["60", "60"] assert [item.group_type for item in data] == ["COUNTERPARTY", "COUNTERPARTY"] def test_outline_wins_over_header_account_and_rows_inherit_header_code(rules: Rules) -> None: """Review Focus 4: счёт в шапке, строк-счетов нет, outline есть — `outline`, но строки наследуют счёт шапки.""" rows: list[list[object]] = [ ["ООО Зерновая"], ["Оборотно-сальдовая ведомость по счету 60"], ["Период: 9 месяцев 2025 г."], [], *_table_head(), [L(0), "ООО А", 100, None, 50, 30, 120, None], [L(1), "Договор 1", 100, None, 50, 30, 120, None], [L(0), "Итого", 100, None, 50, 30, 120, None], ] document, codes = _parse(rules, rows) assert "OSV_HIERARCHY_BY_OUTLINE" in codes assert HEADER_ACCOUNT_CODE not in codes data = [item for item in document.rows if item.group_type != "TOTAL"] assert [item.account_code_raw for item in data] == ["60", "60"]