arm-core/api/tests/test_domain_statement_mapping.py
Andrey Epifancev 234e830476 test(statements): закрыты отложенные minor-находки ревью задач 1-12 (T1.1, T2.1, T3.1, T3.3, T4.x, T5.5, T7.2, T7.3, T8.4, T10.1, T11.3, T12.4)
Проверки §7 по колонкам, допуск, abs_terms, M-9, граница округления; граничные случаи маппинга колонок и
уверенности; шапка, смешанные снимки, статус при задании needs_check, предупреждение о повторе формы и
идемпотентный повторный маппинг форм; редакция формы, пара ББ/ОФР, 403 без права, подтверждение формы без шапки
и без дат; комментарий без видимости ЮЛ; справочник пользователей; documents_count; CSV чек-листа.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 14:00:09 +04:00

212 lines
8.7 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.

"""Чистые функции маппинга формы (spec osv-result-mapping «Документ формы ББ/ОФР»; design D-2, D-3, D-6, D-14)."""
import datetime
from decimal import Decimal
import pytest
from src.contract.result import StatementDocument
from src.domain.statement_mapping import (
build_statement,
edition_for,
quarter_of,
statement_period,
statement_status,
values_ok,
)
from src.models.enums import DocumentCheckResult, DocumentStatus, DocumentType, PeriodQuarter, StatementType
d = datetime.date
@pytest.mark.parametrize(
("day", "quarter"),
[
(d(2025, 3, 31), PeriodQuarter.Q1),
(d(2025, 6, 30), PeriodQuarter.Q2),
(d(2025, 9, 30), PeriodQuarter.Q3),
(d(2024, 12, 31), PeriodQuarter.Q4),
(d(2025, 10, 31), None),
(d(2025, 6, 29), None),
],
)
def test_quarter_of(day: datetime.date, quarter: PeriodQuarter | None) -> None:
assert quarter_of(day) is quarter
def test_period_and_edition() -> None:
assert statement_period(d(2025, 9, 30), [d(2024, 12, 31)]) == d(2025, 9, 30)
assert statement_period(None, [d(2024, 12, 31), d(2025, 6, 30), None]) == d(2025, 6, 30)
assert statement_period(None, [None]) is None
assert edition_for(d(2024, 12, 31)) == "66n"
assert edition_for(d(2025, 3, 31)) == "fsbu4"
assert edition_for(None) is None
def _doc(**overrides: object) -> StatementDocument:
data: dict[str, object] = {
"document_type": "BALANCE",
"locator": "sheet:ББ",
"header": {
"form_name": "Бухгалтерский баланс",
"inn": "7700000000",
"inn_confidence": 100,
"report_date": "2025-09-30",
"report_date_text": "на 30 сентября 2025 г.",
"units": "тыс. руб.",
"organization_name": "ООО Пример",
"okud": "0710001",
},
"columns": [
{"column_index": 0, "label": "На 30.09.2025", "period_date": "2025-09-30"},
{"column_index": 1, "label": "На 31.12.2024", "period_date": "2024-12-31"},
],
"lines": [
{
"line_code": "1150",
"line_code_confidence": 100,
"line_name": "ОС",
"line_name_confidence": 100,
"values": [{"column_index": 0, "amount": "10", "confidence": 100, "is_recognized": True}],
},
{"line_code": "1150", "line_name": "дубль", "values": []},
{
"line_code": "1600",
"line_name": "БАЛАНС",
"values": [{"column_index": 5, "amount": "1", "confidence": 100, "is_recognized": True}],
},
],
}
data.update(overrides)
return StatementDocument.model_validate(data)
def test_build_statement_cells_and_header() -> None:
draft = build_statement(_doc())
expected_cells = 4 # 2 строки × 2 колонки
assert draft.document_type is DocumentType.BALANCE and draft.statement_type is StatementType.balance
assert (draft.period_year, draft.period_quarter, draft.period_date) == (2025, PeriodQuarter.Q3, d(2025, 9, 30))
assert draft.header["inn"] == "7700000000" and draft.header["inn_original_value"] == "7700000000"
assert draft.header["inn_verified"] is True
assert draft.header["report_date_original_value"] == "на 30 сентября 2025 г."
assert draft.header["units"] == draft.header["units_original_value"] == "тыс. руб."
assert draft.columns == [(0, "На 30.09.2025", d(2025, 9, 30), True), (1, "На 31.12.2024", d(2024, 12, 31), False)]
assert [line.line_code for line in draft.lines] == ["1150", "1600"] # дубль 1150 отброшен
cells = {(v.line_code, v.column_index): v for v in draft.values}
assert len(cells) == expected_cells
assert cells[("1150", 0)].amount == Decimal("10") and cells[("1150", 1)].amount is None
assert cells[("1150", 1)].is_recognized is None and cells[("1600", 0)].confidence is None
assert any("1150" in w and "повтор" in w for w in draft.warnings)
assert any("колонк" in w and "5" in w for w in draft.warnings)
def test_value_outside_columns_skipped() -> None:
"""Review Focus 3: значение с column_index вне колонок — пропущено с предупреждением, остальное смаплено."""
draft = build_statement(_doc())
assert all(v.column_index in (0, 1) for v in draft.values)
def test_period_from_columns_and_non_quarter() -> None:
no_date = _doc(header={"form_name": "ББ", "report_date": None})
draft = build_statement(no_date)
assert (draft.period_year, draft.period_quarter) == (2025, PeriodQuarter.Q3)
assert draft.header["report_date"] is None
october = _doc(header={"form_name": "ББ", "report_date": "2025-10-31"})
draft = build_statement(october)
assert (draft.period_year, draft.period_quarter) == (2025, None)
assert any("конец квартала" in w for w in draft.warnings)
def test_no_dates_at_all() -> None:
draft = build_statement(_doc(header={"form_name": "ББ"}, columns=[{"column_index": 0, "label": ""}]))
assert (draft.period_year, draft.period_quarter, draft.period_date) == (None, None, None)
assert draft.columns == [(0, "", None, None)]
def test_status_rules() -> None:
ok = dict(
job_done=True,
checks_passed=True,
counterparty=DocumentCheckResult.MATCH,
period_known=True,
values_ok=True,
has_lines=True,
warnings=[],
)
assert statement_status(**ok) is DocumentStatus.RECOGNIZED # type: ignore[arg-type]
for key, bad in [
("job_done", False),
("checks_passed", False),
("counterparty", DocumentCheckResult.MISMATCH),
("period_known", False),
("values_ok", False),
("has_lines", False),
("warnings", ["x"]),
]:
assert statement_status(**{**ok, key: bad}) is DocumentStatus.NEEDS_CHECK # type: ignore[arg-type]
assert statement_status(**{**ok, "counterparty": DocumentCheckResult.UNKNOWN}) is DocumentStatus.RECOGNIZED # type: ignore[arg-type]
def test_values_ok() -> None:
hundred = Decimal("100")
assert values_ok([(False, True, hundred), (False, None, None)])
assert not values_ok([(False, False, hundred)])
assert not values_ok([(False, True, Decimal("99"))])
assert values_ok([(True, False, Decimal("10"))]) # правленое аналитиком значение не тянет статус вниз
def test_equal_column_dates_current_is_lowest_index_only() -> None:
columns = [
{"column_index": 2, "label": "б", "period_date": "2025-09-30"},
{"column_index": 1, "label": "а", "period_date": "2025-09-30"},
{"column_index": 3, "label": "в", "period_date": "2024-12-31"},
]
draft = build_statement(_doc(columns=columns, lines=[]))
assert {index: current for index, _, _, current in draft.columns} == {1: True, 2: False, 3: False}
def test_column_without_date_next_to_dated_is_not_current() -> None:
columns = [
{"column_index": 0, "label": "примечание"},
{"column_index": 1, "label": "На 30.09.2025", "period_date": "2025-09-30"},
]
draft = build_statement(_doc(columns=columns, lines=[]))
assert {index: current for index, _, _, current in draft.columns} == {0: False, 1: True}
def test_duplicate_code_values_belong_to_first_line() -> None:
lines = [
{
"line_code": "1150",
"line_name": "первая",
"values": [{"column_index": 0, "amount": "10", "confidence": 100, "is_recognized": True}],
},
{
"line_code": "1150",
"line_name": "дубль",
"values": [{"column_index": 0, "amount": "999", "confidence": 100, "is_recognized": True}],
},
]
draft = build_statement(_doc(lines=lines))
assert [line.line_name for line in draft.lines] == ["первая"]
assert [(v.line_code, v.column_index, v.amount) for v in draft.values] == [
("1150", 0, Decimal("10")),
("1150", 1, None),
]
def test_fractional_confidence_kept_exactly() -> None:
lines = [
{
"line_code": "1150",
"line_code_confidence": 99.5,
"line_name": "ОС",
"line_name_confidence": 98.25,
"values": [{"column_index": 0, "amount": "10", "confidence": 99.5, "is_recognized": True}],
}
]
draft = build_statement(_doc(lines=lines))
assert draft.lines[0].line_code_confidence == Decimal("99.5")
assert draft.lines[0].line_name_confidence == Decimal("98.25")
assert draft.values[0].confidence == Decimal("99.5")