Тест test_npz_candidates красный: НПЗ — BLOCKED (Р-31), см. отчёт задачи 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
154 lines
8.5 KiB
Python
154 lines
8.5 KiB
Python
"""Адаптер книги -> `FormCandidate`: план листов, склейка листов-продолжений, предупреждения (план 4, задача 5)."""
|
||
|
||
import pytest
|
||
from gridbuilder import at, sheet_from_rows, with_formats
|
||
|
||
from arm_statements import DocumentType, Severity, Source, default_rules
|
||
from src.parser.deadline import Deadline
|
||
from src.parser.detect import SheetKind, is_osv_sheet
|
||
from src.parser.grid import Sheet
|
||
from src.parser.rules import load_rules
|
||
from src.parser.statements.adapter import build_candidates, plan_sheets
|
||
from src.parser.statements.forms import StatementRules
|
||
|
||
NO_DEADLINE = Deadline(10_000.0)
|
||
STATEMENT = SheetKind.STATEMENT
|
||
CONTINUATION = SheetKind.STATEMENT_CONTINUATION
|
||
BALANCE_CODES = (1110, 1150, 1170, 1100)
|
||
INCOME_CODES = (2110, 2120, 2400)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def rules() -> StatementRules:
|
||
return default_rules()
|
||
|
||
|
||
def _form(title: str, codes: tuple[int, ...], name: str, *, amounts: int = 3, marker: str | None = None) -> Sheet:
|
||
head = [[marker]] if marker else [[title]]
|
||
caption = at({1: "Показатель", 3: "Код", **{4 + i: f"На 3{i + 1}.12.2024" for i in range(amounts)}})
|
||
body = [at({1: "Статья", 3: code, **{4 + i: 10 + i for i in range(amounts)}}) for code in codes]
|
||
return sheet_from_rows([*head, caption, *body], name=name)
|
||
|
||
|
||
def test_balance_with_marked_continuation_and_income(rules: StatementRules) -> None:
|
||
balance = _form("Бухгалтерский баланс", BALANCE_CODES, "Баланс")
|
||
marked = _form("", (1210, 1230, 1600), "Баланс (2)", marker="Форма 0710001 с.2")
|
||
ofr = _form("Отчет о финансовых результатах", INCOME_CODES, "ОФР")
|
||
candidates = build_candidates(
|
||
[(balance, STATEMENT), (marked, CONTINUATION), (ofr, STATEMENT)], rules, NO_DEADLINE, xlsb=False
|
||
)
|
||
assert [(c.type_hint, c.locator) for c in candidates] == [
|
||
(DocumentType.BALANCE, "sheet:Баланс,Баланс (2)"),
|
||
(DocumentType.INCOME, "sheet:ОФР"),
|
||
]
|
||
assert candidates[0].segment_breaks == (balance.n_rows,)
|
||
assert len({len(row) for row in candidates[0].rows}) == 1
|
||
assert candidates[0].source is Source.EXCEL
|
||
assert len(candidates[0].rows) == balance.n_rows + marked.n_rows
|
||
|
||
|
||
def test_refs_of_cells(rules: StatementRules) -> None:
|
||
rows = [at({1: "Бухгалтерский баланс"})] + [[] for _ in range(24)]
|
||
rows.append(at({3: "Код", 4: "На 31.12.2024"}))
|
||
rows.append(at({1: "Статья", 3: 1110, 4: 1}))
|
||
rows += [at({1: "Статья", 3: code, 4: 1}) for code in (1150, 1170, 1100)]
|
||
assert rows[26] == at({1: "Статья", 3: 1110, 4: 1})
|
||
for name, ref in (("Баланс", "Баланс!C27"), ("Бухгалтерский баланс. Лист 1", "'Бухгалтерский баланс. Лист 1'!C27")):
|
||
[candidate] = build_candidates([(sheet_from_rows(rows, name=name), STATEMENT)], rules, NO_DEADLINE, xlsb=False)
|
||
cells = [cell for row in candidate.rows for cell in row if cell is not None and cell.text == "1110"]
|
||
assert [cell.ref for cell in cells] == [ref]
|
||
|
||
|
||
def test_marker_type_conflict_is_separate_candidate(rules: StatementRules) -> None:
|
||
balance = _form("Бухгалтерский баланс", BALANCE_CODES, "Баланс")
|
||
other = _form("", INCOME_CODES, "Лист 2", marker="Форма 0710002 с.2")
|
||
first, second = build_candidates([(balance, STATEMENT), (other, CONTINUATION)], rules, NO_DEADLINE, xlsb=False)
|
||
assert first.warnings == ()
|
||
assert second.type_hint is DocumentType.INCOME
|
||
assert [(w.code, w.severity) for w in second.warnings] == [("CONTINUATION_TYPE_CONFLICT", Severity.ERROR)]
|
||
assert second.segment_breaks == ()
|
||
assert len(second.rows) == other.n_rows
|
||
|
||
|
||
def test_single_line_continuation_joins(rules: StatementRules) -> None:
|
||
ofr = _form("Отчет о финансовых результатах", INCOME_CODES, "ОФР")
|
||
tail = sheet_from_rows([at({1: "Чистая прибыль", 3: 2400, 4: 1, 5: 2, 6: 3})], name="ОФР (2)")
|
||
[candidate] = build_candidates([(ofr, STATEMENT), (tail, CONTINUATION)], rules, NO_DEADLINE, xlsb=False)
|
||
assert candidate.segment_breaks == (ofr.n_rows,)
|
||
assert candidate.warnings == ()
|
||
|
||
|
||
def test_wider_sheet_with_three_codes_is_not_joined(rules: StatementRules) -> None:
|
||
balance = _form("Бухгалтерский баланс", BALANCE_CODES, "Баланс", amounts=3)
|
||
wide = sheet_from_rows(
|
||
[at({1: "Статья", 3: code, 4: 1, 5: 2, 6: 3, 7: 4}) for code in (1210, 1230, 1250)], name="Лист 2"
|
||
)
|
||
first, second = build_candidates([(balance, STATEMENT), (wide, CONTINUATION)], rules, NO_DEADLINE, xlsb=False)
|
||
assert first.segment_breaks == ()
|
||
assert second.type_hint is None
|
||
assert [(w.code, w.severity) for w in second.warnings] == [("SHEET_NOT_JOINED", Severity.ERROR)]
|
||
|
||
|
||
def test_lonely_continuation_has_no_warning(rules: StatementRules) -> None:
|
||
marked = _form("", (1210, 1230, 1600), "Баланс (2)", marker="Форма 0710001 с.2")
|
||
[candidate] = build_candidates([(marked, CONTINUATION)], rules, NO_DEADLINE, xlsb=False)
|
||
assert candidate.type_hint is DocumentType.BALANCE
|
||
assert candidate.warnings == ()
|
||
assert candidate.header.inn is None
|
||
assert len(candidate.rows) == marked.n_rows
|
||
|
||
|
||
def test_two_forms_on_one_sheet_share_locator(rules: StatementRules) -> None:
|
||
balance = [at({1: "Бухгалтерский баланс"}), at({3: "Код", 4: "На 31.12.2024"})]
|
||
balance += [at({1: "Статья", 3: code, 4: 1}) for code in BALANCE_CODES]
|
||
income = [at({1: "Отчет о финансовых результатах"}), at({3: "Код", 4: "За 2024"})]
|
||
income += [at({1: "Статья", 3: code, 4: 1}) for code in INCOME_CODES]
|
||
sheet = sheet_from_rows([*balance, *income], name="Формы")
|
||
candidates = build_candidates([(sheet, STATEMENT)], rules, NO_DEADLINE, xlsb=False)
|
||
assert [(c.type_hint, c.locator) for c in candidates] == [
|
||
(DocumentType.BALANCE, "sheet:Формы"),
|
||
(DocumentType.INCOME, "sheet:Формы"),
|
||
]
|
||
assert len(candidates[0].rows) == len(balance)
|
||
assert len(candidates[1].rows) == len(income)
|
||
|
||
|
||
def test_xlsb_and_unparsed_formats_warnings(rules: StatementRules) -> None:
|
||
balance = _form("Бухгалтерский баланс", BALANCE_CODES, "Баланс", amounts=1)
|
||
tail = _form("", (1210, 1230, 1250), "Баланс (2)", amounts=1, marker="Форма 0710001 с.2")
|
||
formats = {(row, 4): "0.0%" for row in range(3, balance.n_rows + 1)}
|
||
balance = with_formats(balance, formats)
|
||
tail = with_formats(tail, {(row, 4): "0.0%" for row in range(3, tail.n_rows + 1)})
|
||
ofr = _form("Отчет о финансовых результатах", INCOME_CODES, "ОФР", amounts=1)
|
||
candidates = build_candidates(
|
||
[(balance, STATEMENT), (tail, CONTINUATION), (ofr, STATEMENT)], rules, NO_DEADLINE, xlsb=True
|
||
)
|
||
codes = [[w.code for w in c.warnings] for c in candidates]
|
||
assert codes == [["XLSB_NO_FORMATS", "NUMBER_FORMAT_UNPARSED"], ["XLSB_NO_FORMATS"]]
|
||
assert "«0.0%»" in candidates[0].warnings[1].message
|
||
assert candidates[0].warnings[1].message.count("0.0%") == 1
|
||
|
||
|
||
def test_plan_sheets_weak_continuation_only_after_form(rules: StatementRules) -> None:
|
||
ofr = _form("Отчет о финансовых результатах", INCOME_CODES, "ОФР")
|
||
weak = sheet_from_rows([["Чистая прибыль", "2400", 10, 8]], name="2400")
|
||
notes = sheet_from_rows([["Примечания"], ["Текст пояснения к отчетности"]], name="Примечания")
|
||
osv = sheet_from_rows([["Оборотно-сальдовая ведомость"]], name="ОСВ")
|
||
|
||
def is_osv(sheet: Sheet) -> bool:
|
||
return sheet.name == "ОСВ"
|
||
|
||
plan = plan_sheets([ofr, weak], is_osv, rules)
|
||
assert [(s.name, k) for s, k in plan.statements] == [("ОФР", STATEMENT), ("2400", CONTINUATION)]
|
||
plan = plan_sheets([osv, weak], is_osv, rules)
|
||
assert (plan.osv, plan.statements, plan.other) == ((osv,), (), (weak,))
|
||
plan = plan_sheets([ofr, notes, weak], is_osv, rules)
|
||
assert [s.name for s in plan.other] == ["Примечания", "2400"]
|
||
|
||
|
||
def test_is_osv_sheet_by_marker() -> None:
|
||
rules = load_rules()
|
||
osv = sheet_from_rows([["Оборотно-сальдовая ведомость по счету 60"]])
|
||
assert is_osv_sheet(osv, rules)
|
||
assert not is_osv_sheet(sheet_from_rows([["Бухгалтерский баланс"]]), rules)
|