Andrey Epifancev b50ac38a73 fix(statements): улучш. тесты guard для _inn — исключение ИНН аудитора (Fix round 1)
Обнаружена важная находка (T9.I1): test_inn_skips_auditor_block в ONE_C fixture
проходил из-за структуры данных (org INN выше auditor INN), не благодаря guard.
Добавлены два явных guard-теста, которые ПАДАЮТ БЕЗ guard и ПРОХОДЯТ с guard:

(a) test_inn_from_auditor_block_only_is_none — только block аудитора в шапке:
    Без guard (last_row): _inn находит auditor INN = 9999999999 ✗
    С guard (min(last_row, auditor_from-1)): находит Nothing = None ✓

(b) test_inn_from_auditor_above_organization_inn_line — org INN до auditor block:
    Оба случая находят org INN = 1234567890, но без guard мог бы найти auditor ✓

Дополнительно:
- Удалён импорт приватной _FULL_CONFIDENCE; использован литерал 100 с noqa
- Убрана необходимая кавычка в аннотации _report_date (datetime.date | None)
- Все 8 тестов зелены: 2085 passed в make check

Refs #14

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 16:28:02 +04:00

226 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Шапка формы по меткам (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}"