from datetime import date from decimal import Decimal as D from arm_statements.columns import DocumentDates from arm_statements.evaluation import EvalLine, EvalValue from arm_statements.header import parse_header from arm_statements.loader import Rules from arm_statements.model import DocumentType, LineStatus from arm_statements.review import evaluate, review_header from arm_statements.tests.builders import hdr OK, DETAIL, UNREADABLE = LineStatus.OK, LineStatus.DETAIL, LineStatus.UNREADABLE NO_DATES = DocumentDates(None, None, None, None) def ln( # noqa: PLR0913, PLR0917 -- сигнатура задана планом (Task 15) code: str | None, text: str, amount: str | None, conf: float = 100.0, recognized: bool = True, status: LineStatus = OK, order: int = 0, ) -> EvalLine: value = EvalValue("0", None if amount is None else D(amount), text, conf, recognized) return EvalLine(f"{code}-{order}", order, status, code, 100.0, False, (value,)) def flags(lines: list[EvalLine], doc: DocumentType, rules: Rules) -> dict[str, bool]: result = evaluate(lines, ["0"], doc, date(2025, 9, 30), rules) return {key: flag for (key, _), flag in result.value_needs_review.items()} def test_guess_confirmed_by_relation(rules: Rules) -> None: lines = [ ln("1210", "100", "100", order=0), ln("1230", "42831 1", "428311", recognized=False, order=1), ln("1200", "428411", "428411", order=2), ] assert flags(lines, DocumentType.BALANCE, rules)["1230-1"] is False def test_sign_in_curly_bracket_confirmed(rules: Rules) -> None: lines = [ ln("2110", "10000000", "10000000", order=0), ln("2120", "(7744536}", "-7744536", recognized=False, order=1), ln("2100", "2255464", "2255464", order=2), ln("2210", "(100)", "-100", order=3), ln("2220", "(200)", "-200", order=4), ln("2200", "2255164", "2255164", order=5), ] assert flags(lines, DocumentType.INCOME, rules)["2120-1"] is False def test_sign_outside_convention(rules: Rules) -> None: lines = [ ln("2110", "100000", "100000", order=0), ln("2120", "38378", "38378", conf=60.0, order=1), ln("2100", "61622", "61622", order=2), ln("2210", "(100)", "-100", order=3), ln("2220", "(200)", "-200", order=4), ] assert flags(lines, DocumentType.INCOME, rules)["2120-1"] is True def test_null_amount_and_unchecked_code(rules: Rules) -> None: lines = [ln("2120", "{38378)", None, recognized=False, order=0), ln("2900", "15", "15", conf=80.0, order=1)] result = flags(lines, DocumentType.INCOME, rules) assert (result["2120-0"], result["2900-1"]) == (True, True) def test_detail_values_never_flagged(rules: Rules) -> None: lines = [ln("2110", "100", "100", order=0), ln(None, "1 425 130", "1425130", conf=78.0, status=DETAIL, order=1)] assert flags(lines, DocumentType.INCOME, rules)["None-1"] is False def test_soft_relation_covers_only_exactly(rules: Rules) -> None: exact = [ ln("2300", "1000", "1000", order=0), ln("2410", "200", "200", order=1), ln("2400", "800", "800", conf=66.0, order=2), ] assert flags(exact, DocumentType.INCOME, rules)["2400-2"] is False rounding = [ ln("2300", "17000000000", "17000000000", order=0), ln("2410", "0", "0", order=1), ln("2400", "17000000009", "17000000009", conf=66.0, order=2), ] assert flags(rounding, DocumentType.INCOME, rules)["2400-2"] is True def test_hard_failure_flags_clean_cells(rules: Rules) -> None: lines = [ln("1210", "100", "100", order=0), ln("1230", "50", "50", order=1), ln("1200", "999", "999", order=2)] result = evaluate(lines, ["0"], DocumentType.BALANCE, None, rules) assert result.hard_failures >= 1 assert all(result.value_needs_review[(f"{c}-{i}", "0")] for i, c in enumerate(["1210", "1230", "1200"])) accepted = evaluate(lines, ["0"], DocumentType.BALANCE, None, rules, accepted=True) assert not any(accepted.value_needs_review.values()) def test_unreadable_values(rules: Rules) -> None: lines = [ln(None, "7", "7", conf=60.0, status=UNREADABLE), ln(None, "5", "5", status=UNREADABLE, order=1)] result = flags(lines, DocumentType.BALANCE, rules) assert (result["None-0"], result["None-1"]) == (True, False) def test_report_date_confirmed_or_not(rules: Rules) -> None: head = parse_header( hdr(report_date=("30.09.2025", 60.0), organization_name="ООО «Пример»", inn="7700000000", units="тыс. руб."), rules, ) ok_dates = DocumentDates(date(2025, 9, 30), date(2025, 9, 30), date(2025, 9, 30), date(2025, 9, 30)) reviewed, warnings = review_header(head, ok_dates, rules) assert (reviewed.needs_review_fields, warnings) == ((), []) relative_only = DocumentDates(date(2025, 9, 30), None, None, None) reviewed, warnings = review_header(head, relative_only, rules) assert reviewed.needs_review_fields == ("report_date",) found = [(w.code, w.severity.value, w.field) for w in warnings] assert found == [("REPORT_DATE_UNCERTAIN", "error", "report_date")] def test_header_presence_warnings(rules: Rules) -> None: cells = hdr(organization_name="ООО «Пример»", report_date="30.09.2025", units="тыс. руб.") _, no_inn = review_header(parse_header(cells, rules), NO_DATES, rules) assert [w.code for w in no_inn] == ["INN_NOT_FOUND"] _, nothing = review_header(parse_header(hdr(), rules), NO_DATES, rules) assert {w.code for w in nothing} == {"NO_ORGANIZATION_NO_INN", "PERIOD_UNKNOWN", "UNITS_NOT_FOUND"} def test_uncertain_inn_and_unparsed_date(rules: Rules) -> None: cells = hdr(organization_name=("ООО «Пример»", 70.0), inn=("7700000000", 80.0), report_date="дата?", units="руб.") dates = DocumentDates(None, None, date(2025, 9, 30), None) reviewed, warnings = review_header(parse_header(cells, rules), dates, rules) assert reviewed.needs_review_fields == () assert [(w.code, w.field) for w in warnings] == [ ("ORGANIZATION_UNCERTAIN", "organization_name"), ("INN_UNCERTAIN", "inn"), ("REPORT_DATE_UNPARSED", "report_date"), ]