arm-core/api/src/domain/statement_lines.py

97 lines
4.8 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.

"""Словарь имён строк форм ББ/ОФР по редакциям — состав формы для экрана проверки (05-domain-rules §7 «Ядро»;
change statement-template-lines, design D-2) и справочные имена для совпадения наименований (§11.9, Р-20).
Данные — `api/rules/statement_lines.yaml`; порядок строк в списке — порядок формы. Код в редакции может стоять
несколько раз (синонимы и законные повторы, Р-20): состав формы берёт первое имя, сравнение наименований — любое.
Редакцию выбирает `statement_mapping.edition_for`; здесь — только состав. Справочник ничего не убирает и не
переименовывает в форме: строки источника, которых в нём нет, остаются как есть.
"""
import functools
import re
from dataclasses import dataclass
from pathlib import Path
import yaml
__all__ = ["RULES_PATH", "LineCatalog", "TemplateLine", "load_line_catalog"]
RULES_PATH = Path(__file__).resolve().parents[2] / "rules" / "statement_lines.yaml"
FORMS = ("BALANCE", "INCOME")
_CODE = re.compile(r"^\d{4}$")
@dataclass(frozen=True)
class TemplateLine:
code: str
name: str
@dataclass(frozen=True)
class LineCatalog:
"""`editions[редакция][форма]` — все записи словаря в порядке формы, повтор кода допустим."""
editions: dict[str, dict[str, tuple[TemplateLine, ...]]]
def _entries(self, edition: str | None, form: str) -> tuple[TemplateLine, ...]:
if edition is None:
return ()
return self.editions.get(edition, {}).get(form, ())
def template_lines(self, edition: str | None, form: str) -> tuple[TemplateLine, ...]:
"""Строки формы в порядке формы: по одной на код, первое имя, порядок первого появления.
Без редакции или для чужой формы — пусто."""
seen: set[str] = set()
result: list[TemplateLine] = []
for line in self._entries(edition, form):
if line.code not in seen:
seen.add(line.code)
result.append(line)
return tuple(result)
def reference_name(self, edition: str | None, form: str, code: str) -> str | None:
"""Первое имя кода в редакции (P3-11); кода нет — `None`."""
for line in self._entries(edition, form):
if line.code == code:
return line.name
return None
def names(self, form: str, code: str) -> tuple[str, ...]:
"""Все имена кода во всех редакциях без повторов, в порядке появления."""
result: dict[str, None] = {}
for forms in self.editions.values():
for line in forms.get(form, ()):
if line.code == code:
result.setdefault(line.name)
return tuple(result)
def edition_codes(self, edition: str, form: str) -> frozenset[str]:
"""Коды формы в редакции."""
return frozenset(line.code for line in self._entries(edition, form))
def _form_lines(edition: str, form: str, items: object) -> tuple[TemplateLine, ...]:
if not isinstance(items, list):
raise ValueError(f"редакция {edition}: нет списка строк формы {form}")
lines = tuple(TemplateLine(str(item["code"]), " ".join(str(item["name"]).split())) for item in items)
for line in lines:
if not _CODE.fullmatch(line.code):
raise ValueError(f"редакция {edition}, форма {form}: код {line.code!r} — не четыре цифры")
return lines
@functools.lru_cache(maxsize=4)
def load_line_catalog(path: Path | None = None) -> LineCatalog:
"""Справочник из yaml; любая ошибка формата — `ValueError` с именем файла."""
source = path or RULES_PATH
try:
data = yaml.safe_load(source.read_text(encoding="utf-8"))
editions: dict[str, dict[str, tuple[TemplateLine, ...]]] = {}
for edition in data["editions"]:
edition_id = str(edition["id"])
forms = edition["forms"]
editions[edition_id] = {form: _form_lines(edition_id, form, forms.get(form)) for form in FORMS}
return LineCatalog(editions=editions)
except (OSError, KeyError, TypeError, AttributeError, ValueError, yaml.YAMLError) as exc:
raise ValueError(f"справочник строк {source.name}: {exc}") from exc