Andrey Epifancev 412a322b9d chore(arm_statements): копия пакета arm-statements@6bce80b (#52)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:38:43 +04:00

172 lines
7.5 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.

"""Шаг 8: `needs_review` значений и строк, проверки полей шапки, сводная `evaluate`."""
from collections import Counter
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, replace
from datetime import date
from arm_statements.amounts import sign_form_of
from arm_statements.checks import RelationOutcome, evaluate_relations, form_variant
from arm_statements.columns import DocumentDates
from arm_statements.evaluation import EvalLine, EvalValue, is_check_line
from arm_statements.issues import WarningCode, make_warning
from arm_statements.loader import Rules
from arm_statements.model import (
DocumentType,
Edition,
FormVariant,
HeaderField,
LineStatus,
SignForm,
StatementHeader,
StatementWarning,
)
from arm_statements.structure import StructureResult, confirm_codes
_CONFIDENT = 100
@dataclass(frozen=True, slots=True)
class Evaluation:
variant: FormVariant | None
edition: Edition | None
outcomes: tuple[RelationOutcome, ...]
line_needs_review: Mapping[str, bool]
value_needs_review: Mapping[tuple[str, str], bool]
hard_failures: int
def _needs_coverage(value: EvalValue) -> bool:
return (value.confidence is not None and value.confidence < _CONFIDENT) or not value.is_recognized
def _is_covered(line: EvalLine, value: EvalValue, outcomes: Sequence[RelationOutcome]) -> bool:
if value.amount is None or line.line_code is None:
return False
for item in outcomes:
if item.column != value.column or line.line_code not in item.codes or not item.computed:
continue
if item.within_tolerance or (item.rule.strict and item.within_rounding):
return True
return False
def _sign_confirmed(
line: EvalLine, value: EvalValue, lines: Sequence[EvalLine], abs_codes: frozenset[str], rules: Rules
) -> bool:
"""Форма знака ячейки совпадает с формой строгого большинства прочих членов `abs_terms` колонки."""
forms: Counter[SignForm] = Counter()
for other in lines:
if other is line or not is_check_line(other) or other.line_code not in abs_codes:
continue
for item in other.values:
if item.column != value.column or item.amount is None or not item.is_recognized or item.fixed:
continue
form = sign_form_of(item.text, rules)
if form is not None:
forms[form] += 1
total = sum(forms.values())
own = sign_form_of(value.text, rules)
return own is not None and forms[own] * 2 > total
def value_needs_review(
lines: Sequence[EvalLine],
outcomes: Sequence[RelationOutcome],
abs_codes: frozenset[str],
rules: Rules,
*,
accepted: bool = False,
) -> dict[tuple[str, str], bool]:
hard_codes: set[tuple[str, str]] = set()
if not accepted:
for item in outcomes:
if item.hard_failed:
hard_codes |= {(item.column, code) for code in item.codes}
result: dict[tuple[str, str], bool] = {}
for line in lines:
for value in line.values:
key = (line.key, value.column)
if line.line_status is LineStatus.DETAIL or value.fixed:
result[key] = False
elif is_check_line(line) and (value.column, line.line_code or "") in hard_codes:
result[key] = True
elif not _needs_coverage(value):
result[key] = False
else:
covered = _is_covered(line, value, outcomes)
signed = line.line_code not in abs_codes or _sign_confirmed(line, value, lines, abs_codes, rules)
result[key] = not (covered and signed)
return result
def line_needs_review(lines: Sequence[EvalLine], structure: StructureResult) -> dict[str, bool]:
return {
line.key: line.key in structure.unconfirmed and not line.code_fixed and line.line_status is LineStatus.OK
for line in lines
}
def evaluate( # noqa: PLR0913 -- сигнатура задана публичным API пакета
lines: Sequence[EvalLine],
columns: Sequence[str],
document_type: DocumentType,
effective: date | None,
rules: Rules,
*,
accepted: bool = False,
) -> Evaluation:
codes = [item.line_code for item in lines if is_check_line(item) and item.line_code is not None]
variant = form_variant(document_type, codes)
outcomes = evaluate_relations(lines, columns, document_type, variant, rules)
structure = confirm_codes(lines, document_type, variant, effective, rules)
abs_codes = rules.checks.abs_codes(document_type, variant or FormVariant.FULL)
return Evaluation(
variant=variant,
edition=structure.edition,
outcomes=tuple(outcomes),
line_needs_review=line_needs_review(lines, structure),
value_needs_review=value_needs_review(lines, outcomes, abs_codes, rules, accepted=accepted),
hard_failures=sum(1 for item in outcomes if item.hard_failed),
)
def _uncertain(field: HeaderField) -> bool:
return field.confidence is not None and field.confidence < _CONFIDENT
def review_header(
header: StatementHeader, dates: DocumentDates, rules: Rules
) -> tuple[StatementHeader, list[StatementWarning]]:
"""Поля шапки (спека «Шаг 8 — поля шапки»). Для `DISCLOSURE` не вызывается: предупреждений шапки там нет (P1-6)."""
warnings: list[StatementWarning] = []
review_fields: list[str] = []
org, inn, report = header.organization_name, header.inn, header.report_date
def warn(code: WarningCode, message: str, field: str | None = None) -> None:
warnings.append(make_warning(code, message, rules, field=field))
if org.value is None and inn.value is None:
warn(WarningCode.NO_ORGANIZATION_NO_INN, "В шапке нет ни организации, ни ИНН")
elif inn.value is None:
warn(WarningCode.INN_NOT_FOUND, "ИНН не найден", "inn")
elif org.value is None:
warn(WarningCode.ORGANIZATION_NOT_FOUND, "Организация не найдена", "organization_name")
if org.value is not None and _uncertain(org):
warn(WarningCode.ORGANIZATION_UNCERTAIN, "Название организации распознано неуверенно", "organization_name")
if inn.value is not None and _uncertain(inn):
warn(WarningCode.INN_UNCERTAIN, "ИНН распознан неуверенно", "inn")
if report.value is not None:
confidence = report.confidence
if confidence is not None and confidence < _CONFIDENT and report.value != dates.confident_latest_absolute:
review_fields.append("report_date")
warn(WarningCode.REPORT_DATE_UNCERTAIN, "Дата отчёта неуверенна и не подтверждена колонками", "report_date")
elif report.text is not None and report.text.strip() and dates.latest_absolute is not None:
warn(WarningCode.REPORT_DATE_UNPARSED, "Дата отчёта не разобрана", "report_date")
if report.value is None and dates.latest_absolute is None:
warn(WarningCode.PERIOD_UNKNOWN, "Отчётный период не определён: нет ни даты отчёта, ни дат колонок")
if header.units.value is None:
warn(WarningCode.UNITS_NOT_FOUND, "Единицы измерения не найдены", "units")
return replace(header, needs_review_fields=tuple(review_fields)), warnings