140 lines
6.1 KiB
Python
140 lines
6.1 KiB
Python
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"),
|
||
]
|