Обнаружена важная находка (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
226 lines
11 KiB
Python
226 lines
11 KiB
Python
"""Шапка формы по меткам (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}"
|