"""Шапка формы по меткам (design D-5; spec «Шапка документа»).""" import datetime import pytest from gridbuilder import at, sheet_from_rows from src.parser.rules import Rules, load_rules from src.parser.statements.detect import find_forms from src.parser.statements.header import parse_header @pytest.fixture(scope="module") def rules() -> Rules: return load_rules() ONE_C = [ [*at({9: "Бухгалтерский баланс"})], [*at({9: "на 31 декабря 2024 г."})], [*at({34: "Коды"})], [*at({33: "Форма по ОКУД", 34: "0710001"})], [*at({33: "Дата (число, месяц, год)", 34: "31", 37: "12", 40: "2024"})], [*at({2: "Организация", 4: 'Акционерное общество "Унагранде Компани"', 33: "по ОКПО", 34: "47888105"})], [*at({2: "Идентификационный номер налогоплательщика", 33: "ИНН", 34: "3249004697"})], [ *at( { 2: "Вид экономической\nдеятельности", 7: "Производство сыра", 30: "по\nОКВЭД 2", 34: "10.51.3", } ) ], [*at({2: "Организационно-правовая форма / форма собственности", 34: "12267", 39: "16"})], [*at({2: "Непубличные акционерные общества", 14: " /", 15: "Частная собственность", 33: "по ОКОПФ / ОКФС"})], [*at({2: "Единица измерения:", 7: "в тыс. рублей", 33: "по ОКЕИ", 34: "384"})], [*at({2: "Местонахождение (адрес)"})], [*at({2: "242440, Брянская обл, г Севск, ул Ленина, д. 130"})], [*at({2: "Наименование аудиторской организации"})], [*at({2: 'АО "ЮНИКОН"'})], [*at({2: "Идентификационный номер налогоплательщика аудиторской организации", 33: "ИНН", 34: "7716021332"})], [*at({3: "Пояснения", 5: "Наименование показателя", 22: "Код", 25: "На 31 декабря 2024 г."})], [*at({5: "Нематериальные активы", 22: "1110", 25: 1})], ] def _block(sheet, rules): # type: ignore[no-untyped-def] return find_forms(sheet, rules)[0][0] def test_one_c_header(rules: Rules) -> None: sheet = sheet_from_rows(ONE_C) header, warnings = parse_header(sheet, rules, 1, 16, _block(sheet, rules)) assert header.form_name == "Бухгалтерский баланс" and header.okud == "0710001" assert header.organization_name == 'Акционерное общество "Унагранде Компани"' assert header.inn == "3249004697" # Review Focus 2: не ИНН аудитора assert header.okpo == "47888105" assert header.okopf == "12267" and header.okfs == "16" assert header.units == "в тыс. рублей" assert header.report_date_text == "на 31 декабря 2024 г." and header.report_date == datetime.date(2024, 12, 31) assert header.address == "242440, Брянская обл, г Севск, ул Ленина, д. 130" assert header.organization_name_confidence == 100 and header.inn_confidence == 100 # noqa: PLR2004 assert warnings == [] def test_organization_above_label_and_income_period(rules: Rules) -> None: sheet = sheet_from_rows( [ [*at({3: "Отчет о финансовых результатах"})], [*at({3: "за январь - сентябрь 2025 г."})], [*at({13: "Форма по ОКУД", 14: "0710002"})], [*at({7: "Отчетная дата\n(число, месяц, год)", 14: "30", 15: "09", 16: "2025"})], [*at({7: 'ООО "НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД"'})], [*at({3: "Организация:", 13: "по ОКПО", 14: "16098811"})], [*at({3: "Единица измерения", 8: "тыс. руб.", 13: "по ОКЕИ", 14: "384"})], [ *at( { 3: "Пояснения", 5: "Наименование показателя", 10: "Код", 11: "За январь - сентябрь 2025 г.", } ) ], ] ) header, _ = parse_header(sheet, rules, 1, 7, _block(sheet, rules)) assert header.organization_name == 'ООО "НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД"' assert header.inn is None assert header.okpo == "16098811" assert header.units == "тыс. руб." assert header.report_date == datetime.date(2025, 9, 30) assert header.report_date_text == "за январь - сентябрь 2025 г." def test_date_from_numeric_triple_and_name_below_label(rules: Rules) -> None: sheet = sheet_from_rows( [ [*at({2: "БУХГАЛТЕРСКИЙ БАЛАНС"})], [*at({17: "Форма по ОКУД", 18: "0710001"})], [*at({17: "Дата (число, месяц, год)", 18: 30, 19: 9, 20: 2025})], [ *at( { 2: "Организация (орган исполнительной власти)", 7: 'ООО "АГРОФИРМА "МЕЖЕНИНОВСКАЯ"', 17: "по ОКПО", 18: "85262693", } ) ], [*at({2: "Идентификационный номер налогоплательщика", 17: "ИНН", 18: "7016006635"})], [*at({2: "Организационно-правовая форма***", 7: "ООО", 15: "по ОКОПФ/\nОКФС", 18: "65", 19: "16"})], [*at({2: "Единица измерения:", 7: "тыс. руб", 17: "по ОКЕИ", 18: "384"})], [*at({2: "Местонахождение (адрес)", 7: "636141, Томская обл"})], ] ) header, _ = parse_header(sheet, rules, 1, 8, _block(sheet, rules)) assert header.report_date == datetime.date(2025, 9, 30) assert header.report_date_text is None assert header.inn == "7016006635" assert header.okopf == "65" and header.okfs == "16" assert header.address == "636141, Томская обл" def test_continuation_header_is_empty(rules: Rules) -> None: sheet = sheet_from_rows( [ [*at({17: "Форма 0710001 с.2"})], ["Пояснения", "Наименование показателя", "Код", "На 31 декабря 2024 г."], ] ) header, warnings = parse_header(sheet, rules, 1, 1, None) assert header.form_name == "Форма 0710001 с.2" assert header.organization_name is None and header.inn is None assert header.report_date is None and warnings == [] def test_missing_units_and_organization_warn(rules: Rules) -> None: sheet = sheet_from_rows([["Бухгалтерский баланс"], ["на 31 декабря 2024 г."]]) header, warnings = parse_header(sheet, rules, 1, 2, _block(sheet, rules)) assert header.units is None and header.organization_name is None assert any("единица измерения" in warning for warning in warnings) assert any("организация" in warning for warning in warnings) def test_inn_skips_auditor_block(rules: Rules) -> None: """Review Focus: ИНН аудитора (строка 16) не должен попасть в header.inn.""" sheet = sheet_from_rows(ONE_C) header, _ = parse_header(sheet, rules, 1, 16, _block(sheet, rules)) assert header.inn == "3249004697", f"Expected organization INN 3249004697, got {header.inn}" assert header.inn != "7716021332", "Auditor INN 7716021332 should not be included" def test_inn_from_auditor_block_only_is_none(rules: Rules) -> None: """Guard test (a): without org INN, only auditor block ИНН — should be None.""" sheet = sheet_from_rows( [ [*at({3: "Отчет о финансовых результатах"})], [*at({3: "за январь - сентябрь 2025 г."})], [*at({13: "Форма по ОКУД", 14: "0710002"})], [*at({7: 'ООО "ПРОКАТНЫЙ ЗАВОД"'})], [*at({3: "Организация:", 13: "по ОКПО", 14: "16098811"})], [*at({3: "Единица измерения", 8: "тыс. руб.", 13: "по ОКЕИ", 14: "384"})], [*at({2: "Наименование аудиторской организации"})], [*at({2: 'АО "АУДИТ-КОМПАНИЯ"'})], [ *at({ 2: "Идентификационный номер налогоплательщика аудиторской организации", 33: "ИНН", 34: "9999999999", }) ], ] ) header, _ = parse_header(sheet, rules, 1, 9, _block(sheet, rules)) assert header.inn is None, f"Should not pick up auditor INN 9999999999, got {header.inn}" def test_inn_from_auditor_above_organization_inn_line(rules: Rules) -> None: """Guard test (b): org INN before auditor block; guard excludes auditor INN. Organization section (rows 1-4) has INN 1234567890. Auditor block starts at row 5 and has INN 7716021332. Guard searches up to row 4, finds org INN. """ sheet = sheet_from_rows( [ [*at({3: "Бухгалтерский баланс"})], [*at({3: "на 31 декабря 2024 г."})], [*at({7: 'ООО "ЗАВОД"'})], [*at({2: "Идентификационный номер налогоплательщика", 33: "ИНН", 34: "1234567890"})], [*at({2: "Наименование аудиторской организации"})], [*at({2: 'АО "ЮНИКОН"'})], [ *at({ 2: "Идентификационный номер налогоплательщика аудиторской организации", 33: "ИНН", 34: "7716021332", }) ], [*at({2: "Единица измерения", 7: "в тыс. рублей", 33: "по ОКЕИ", 34: "384"})], ] ) header, _ = parse_header(sheet, rules, 1, 8, _block(sheet, rules)) assert ( header.inn == "1234567890" ), f"Should pick organization INN 1234567890, not auditor INN 7716021332, got {header.inn}"