"""Шаг 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