Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
152 lines
6.3 KiB
Python
152 lines
6.3 KiB
Python
"""Перевод выхода пакета в форму контракта v2.0 и вход статуса из документа контракта.
|
||
|
||
Один перевод на оба сервиса (P1-26): хосты свой `contract_map` не пишут. Схему пакет не читает —
|
||
форма держится тестами и разовой сверкой со `statementDocument` схемы v2.0.
|
||
"""
|
||
|
||
from collections.abc import Mapping
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from arm_statements.model import (
|
||
HEADER_FIELDS,
|
||
DocumentType,
|
||
FailedRelation,
|
||
LineStatus,
|
||
PeriodKind,
|
||
Severity,
|
||
StatementDocument,
|
||
StatementHeader,
|
||
StatementWarning,
|
||
)
|
||
from arm_statements.status import StatusInput, StatusLine, StatusValue
|
||
|
||
|
||
def _money(value: Decimal | None) -> str | None:
|
||
return None if value is None else format(value, "f")
|
||
|
||
|
||
def warning_to_contract(warning: StatementWarning, *, file_level: bool = False) -> dict[str, Any]:
|
||
"""`warning` схемы; при `file_level` — `fileWarning` (с `locator`). Ключи без значения опускаются."""
|
||
data: dict[str, Any] = {
|
||
"code": warning.code,
|
||
"severity": str(warning.severity),
|
||
"message": warning.message,
|
||
}
|
||
if warning.line_refs:
|
||
data["line_refs"] = list(warning.line_refs)
|
||
if warning.column_index is not None:
|
||
data["column_index"] = warning.column_index
|
||
if warning.field is not None:
|
||
data["field"] = warning.field
|
||
if file_level and warning.locator is not None:
|
||
data["locator"] = warning.locator
|
||
return data
|
||
|
||
|
||
def _header(header: StatementHeader) -> dict[str, Any]:
|
||
data: dict[str, Any] = {"form_name": header.form_name}
|
||
for name in HEADER_FIELDS:
|
||
field = getattr(header, name)
|
||
value = field.value
|
||
data[name] = value.isoformat() if name == "report_date" and value is not None else value
|
||
data[f"{name}_text"] = field.text
|
||
data[f"{name}_confidence"] = field.confidence
|
||
data["needs_review_fields"] = list(header.needs_review_fields)
|
||
return data
|
||
|
||
|
||
def _relation(relation: FailedRelation) -> dict[str, Any]:
|
||
if relation.column_index is None:
|
||
raise ValueError(f"у соотношения {relation.relation!r} нет column_index: у сервиса номер колонки есть всегда")
|
||
return {
|
||
"column_index": relation.column_index,
|
||
"relation": relation.relation,
|
||
"codes": list(relation.codes),
|
||
"expected": _money(relation.expected),
|
||
"actual": _money(relation.actual),
|
||
}
|
||
|
||
|
||
def to_contract(document: StatementDocument) -> dict[str, Any]:
|
||
"""`statementDocument` схемы v2.0 как json-совместимый `dict`."""
|
||
data: dict[str, Any] = {
|
||
"document_type": str(document.document_type),
|
||
"form_variant": None if document.form_variant is None else str(document.form_variant),
|
||
"locator": document.locator,
|
||
"status": str(document.status),
|
||
"warnings": [warning_to_contract(w) for w in document.warnings],
|
||
"header": _header(document.header),
|
||
"columns": [
|
||
{
|
||
"column_index": c.column_index,
|
||
"label": c.label,
|
||
"label_confidence": c.label_confidence,
|
||
"period_date": None if c.period_date is None else c.period_date.isoformat(),
|
||
"period_kind": str(c.period_kind),
|
||
}
|
||
for c in document.columns
|
||
],
|
||
"lines": [
|
||
{
|
||
"row_index": ln.row_index,
|
||
"ref": ln.ref,
|
||
"line_status": str(ln.line_status),
|
||
"line_code": ln.line_code,
|
||
"line_code_text": ln.line_code_text,
|
||
"line_code_confidence": ln.line_code_confidence,
|
||
"line_name": ln.line_name,
|
||
"line_name_confidence": ln.line_name_confidence,
|
||
"parent_ref": ln.parent_ref,
|
||
"needs_review": ln.needs_review,
|
||
"values": [
|
||
{
|
||
"column_index": v.column_index,
|
||
"amount": _money(v.amount),
|
||
"string_value": v.string_value,
|
||
"text": v.text,
|
||
"confidence": v.confidence,
|
||
"is_recognized": v.is_recognized,
|
||
"needs_review": v.needs_review,
|
||
}
|
||
for v in ln.values
|
||
],
|
||
}
|
||
for ln in document.lines
|
||
],
|
||
}
|
||
if document.checks is not None:
|
||
data["checks"] = {
|
||
"relations_failed": [_relation(r) for r in document.checks.relations_failed],
|
||
"soft_relations_failed": [_relation(r) for r in document.checks.soft_relations_failed],
|
||
}
|
||
return data
|
||
|
||
|
||
def status_input_from_contract(document: Mapping[str, Any]) -> StatusInput:
|
||
"""Вход функции статуса из `statementDocument` контракта v2.0 (то же правило, что `status_input`).
|
||
|
||
Схему не проверяет: битый документ — `KeyError` / `ValueError`. Отсутствующий необязательный ключ и `null` равны.
|
||
"""
|
||
header = document["header"]
|
||
lines = document["lines"]
|
||
columns = document["columns"]
|
||
period_known = header.get("report_date") is not None or any(
|
||
c["period_kind"] == PeriodKind.ABSOLUTE and c.get("period_date") for c in columns
|
||
)
|
||
checks = document.get("checks")
|
||
return StatusInput(
|
||
document_type=DocumentType(document["document_type"]),
|
||
layout_found=len(lines) > 0,
|
||
has_organization_or_inn=bool(header.get("organization_name") or header.get("inn")),
|
||
period_known=period_known,
|
||
needs_review_fields=tuple(header.get("needs_review_fields") or ()),
|
||
lines=tuple(StatusLine(LineStatus(ln["line_status"]), bool(ln["needs_review"])) for ln in lines),
|
||
values=tuple(
|
||
StatusValue(LineStatus(ln["line_status"]), bool(v["needs_review"])) for ln in lines for v in ln["values"]
|
||
),
|
||
hard_failures=0 if checks is None else len(checks["relations_failed"]),
|
||
accepted=False,
|
||
warning_severities=tuple(Severity(w["severity"]) for w in document["warnings"]),
|
||
)
|