Andrey Epifancev cb0bd062d4 test(statements): test_inn_not_taken_from_auditor_block_above_organization_inn (Fix round 3)
Добавлен тест, проверяющий, что ИНН аудитора НИКОГДА не берётся, даже когда
аудиторский блок расположен РАНЬШЕ (выше) строк с ИНН организации.

Лист:
- Строка 3-5: блок аудитора (auditor_from=3)
  - Строка 4: аудиторский ИНН = 7716021332
- Строки 6-8: организация
  - Строка 8: org ИНН = 3249004697

С охраной min(last_row, auditor_from-1) = min(9, 2) = 2:
  Поиск в строках 1-2, ИНН не найден → header.inn is None ✓ GREEN

Без охраны (поиск в строках 1-9):
  Аудиторский ИНН найден первым → header.inn = 7716021332 ✗ RED

Этот тест доказывает, что guard критически важен для исключения аудитора.

Refs #14

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

231 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_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_not_taken_from_auditor_block_above_organization_inn(rules: Rules) -> None:
"""Guard test (b): auditor block ABOVE org INN; guard excludes auditor INN.
Layout (intentionally reversed from real files):
- Auditor block starts at row 3 (auditor_from=3)
- Auditor INN at row 4: value 7716021332
- Organization INN at row 8: value 3249004697
With guard (search 1..auditor_from-1=1..2): Neither INN found → header.inn is None ✓
Without guard (search 1..last_row=1..9): Auditor INN at row 4 found first → **FAIL** ✗
This test verifies auditor INN is never taken, even when it appears before org INN.
"""
sheet = sheet_from_rows(
[
[*at({3: "Бухгалтерский баланс"})],
[*at({3: "на 31 декабря 2024 г."})],
[*at({2: "Наименование аудиторской организации"})],
[*at({2: 'АО "ЮНИКОН"'})],
[
*at(
{
2: "Идентификационный номер налогоплательщика аудиторской организации",
33: "ИНН",
34: "7716021332",
}
)
],
[*at({7: 'ООО "ЗАВОД"'})],
[*at({3: "Организация", 7: 'ООО "ЗАВОД"', 13: "по ОКПО", 14: "16098811"})],
[*at({2: "Идентификационный номер налогоплательщика", 33: "ИНН", 34: "3249004697"})],
[*at({2: "Единица измерения", 7: "в тыс. рублей", 33: "по ОКЕИ", 34: "384"})],
]
)
header, _ = parse_header(sheet, rules, 1, 9, _block(sheet, rules))
assert header.inn is None, (
f"Auditor INN 7716021332 at row 4 is above auditor_from boundary; "
f"org INN 3249004697 at row 8 is below. Guard excludes both. "
f"Got {header.inn}"
)