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

152 lines
6.3 KiB
Python
Raw Permalink 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.

"""Перевод выхода пакета в форму контракта 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"]),
)