172 lines
7.5 KiB
Python
172 lines
7.5 KiB
Python
"""Шаг 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
|