feat(statements): справочник строк форм ББ/ОФР по редакциям (#80)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
381c373467
commit
438752f035
132
api/rules/statement_lines.yaml
Normal file
132
api/rules/statement_lines.yaml
Normal file
@ -0,0 +1,132 @@
|
||||
# Справочник строк форм ББ/ОФР по редакциям: полный состав формы для экрана проверки (05-domain-rules §7 «Ядро»,
|
||||
# design D-2 change statement-template-lines). Порядок строк в списке — порядок формы.
|
||||
# Редакция выбирается по дате периода документа (edition_for): 66n — до 2024-12-31 включительно, fsbu4 — позже.
|
||||
# Источник обеих редакций — шаблоны банка fixtures/statements-xlsx/templates/ («ББ/ОФР до 01.01.2025», «… после
|
||||
# 01.01.2025»; решение владельца 09.10.2026, данные OQ-150 для ядра); склейки наименований с заголовками разделов и
|
||||
# сносками исправлены (design R-2). Копия словаря scan-gateway (api/rules/statement_lines.yaml) с ним не совпадает:
|
||||
# сведение копий — follow-up.
|
||||
editions:
|
||||
- id: 66n
|
||||
forms:
|
||||
BALANCE:
|
||||
- {code: "1110", name: "Нематериальные активы"}
|
||||
- {code: "1120", name: "Результаты исследований и разработок"}
|
||||
- {code: "1130", name: "Нематериальные поисковые активы"}
|
||||
- {code: "1140", name: "Материальные поисковые активы"}
|
||||
- {code: "1150", name: "Основные средства"}
|
||||
- {code: "1160", name: "Доходные вложения в материальные ценности"}
|
||||
- {code: "1170", name: "Финансовые вложения"}
|
||||
- {code: "1180", name: "Отложенные налоговые активы"}
|
||||
- {code: "1190", name: "Прочие внеоборотные активы"}
|
||||
- {code: "1100", name: "Итого по разделу I"}
|
||||
- {code: "1210", name: "Запасы"}
|
||||
- {code: "1220", name: "Налог на добавленную стоимость по приобретенным ценностям"}
|
||||
- {code: "1230", name: "Дебиторская задолженность"}
|
||||
- {code: "1240", name: "Финансовые вложения (за исключением денежных эквивалентов)"}
|
||||
- {code: "1250", name: "Денежные средства и денежные эквиваленты"}
|
||||
- {code: "1260", name: "Прочие оборотные активы"}
|
||||
- {code: "1200", name: "Итого по разделу II"}
|
||||
- {code: "1600", name: "БАЛАНС"}
|
||||
- {code: "1310", name: "Уставный капитал (складочный капитал, уставный фонд, вклады товарищей)"}
|
||||
- {code: "1320", name: "Собственные акции, выкупленные у акционеров"}
|
||||
- {code: "1340", name: "Переоценка внеоборотных активов"}
|
||||
- {code: "1350", name: "Добавочный капитал (без переоценки)"}
|
||||
- {code: "1360", name: "Резервный капитал"}
|
||||
- {code: "1370", name: "Нераспределенная прибыль (непокрытый убыток)"}
|
||||
- {code: "1300", name: "Итого по разделу III"}
|
||||
- {code: "1410", name: "Заемные средства"}
|
||||
- {code: "1420", name: "Отложенные налоговые обязательства"}
|
||||
- {code: "1430", name: "Оценочные обязательства"}
|
||||
- {code: "1450", name: "Прочие обязательства"}
|
||||
- {code: "1400", name: "Итого по разделу IV"}
|
||||
- {code: "1510", name: "Заемные средства"}
|
||||
- {code: "1520", name: "Кредиторская задолженность"}
|
||||
- {code: "1530", name: "Доходы будущих периодов"}
|
||||
- {code: "1540", name: "Оценочные обязательства"}
|
||||
- {code: "1550", name: "Прочие обязательства"}
|
||||
- {code: "1500", name: "Итого по разделу V"}
|
||||
- {code: "1700", name: "БАЛАНС"}
|
||||
INCOME:
|
||||
- {code: "2110", name: "Выручка"}
|
||||
- {code: "2120", name: "Себестоимость продаж"}
|
||||
- {code: "2100", name: "Валовая прибыль (убыток)"}
|
||||
- {code: "2210", name: "Коммерческие расходы"}
|
||||
- {code: "2220", name: "Управленческие расходы"}
|
||||
- {code: "2200", name: "Прибыль (убыток) от продаж"}
|
||||
- {code: "2310", name: "Доходы от участия в других организациях"}
|
||||
- {code: "2320", name: "Проценты к получению"}
|
||||
- {code: "2330", name: "Проценты к уплате"}
|
||||
- {code: "2340", name: "Прочие доходы"}
|
||||
- {code: "2350", name: "Прочие расходы"}
|
||||
- {code: "2300", name: "Прибыль (убыток) до налогообложения"}
|
||||
- {code: "2410", name: "Налог на прибыль"}
|
||||
- {code: "2411", name: "в т.ч. текущий налог на прибыль"}
|
||||
- {code: "2412", name: "отложенный налог на прибыль"}
|
||||
- {code: "2460", name: "Прочее"}
|
||||
- {code: "2400", name: "Чистая прибыль (убыток)"}
|
||||
- {code: "2510", name: "Результат от переоценки внеоборотных активов, не включаемый в чистую прибыль (убыток) периода"}
|
||||
- {code: "2520", name: "Результат от прочих операций, не включаемый в чистую прибыль (убыток) периода"}
|
||||
- {code: "2500", name: "Совокупный финансовый результат периода"}
|
||||
- {code: "2900", name: "Базовая прибыль (убыток) на акцию"}
|
||||
- {code: "2910", name: "Разводненная прибыль (убыток) на акцию"}
|
||||
- id: fsbu4
|
||||
forms:
|
||||
BALANCE:
|
||||
- {code: "1110", name: "Нематериальные активы"}
|
||||
- {code: "1150", name: "Основные средства"}
|
||||
- {code: "1160", name: "Инвестиционная недвижимость"}
|
||||
- {code: "1170", name: "Финансовые вложения"}
|
||||
- {code: "1180", name: "Отложенные налоговые активы"}
|
||||
- {code: "1190", name: "Прочие внеоборотные активы"}
|
||||
- {code: "1100", name: "Итого по разделу I"}
|
||||
- {code: "1210", name: "Запасы"}
|
||||
- {code: "1215", name: "Долгосрочные активы к продаже"}
|
||||
- {code: "1220", name: "Налог на добавленную стоимость по приобретенным ценностям"}
|
||||
- {code: "1230", name: "Дебиторская задолженность"}
|
||||
- {code: "1240", name: "Финансовые вложения (за исключением денежных эквивалентов)"}
|
||||
- {code: "1250", name: "Денежные средства и денежные эквиваленты"}
|
||||
- {code: "1260", name: "Прочие оборотные активы"}
|
||||
- {code: "1200", name: "Итого по разделу II"}
|
||||
- {code: "1600", name: "БАЛАНС"}
|
||||
- {code: "1310", name: "Уставный капитал (складочный капитал, уставный фонд, вклады товарищей)"}
|
||||
- {code: "1320", name: "Собственные акции, принадлежащие обществу, задолженность акционеров по оплате акций"}
|
||||
- {code: "1340", name: "Накопленная дооценка внеоборотных активов"}
|
||||
- {code: "1350", name: "Добавочный капитал (без накопленной дооценки)"}
|
||||
- {code: "1360", name: "Резервный капитал"}
|
||||
- {code: "1370", name: "Нераспределенная прибыль (непокрытый убыток)"}
|
||||
- {code: "1300", name: "Итого по разделу III"}
|
||||
- {code: "1410", name: "Заемные средства"}
|
||||
- {code: "1420", name: "Отложенные налоговые обязательства"}
|
||||
- {code: "1430", name: "Оценочные обязательства"}
|
||||
- {code: "1450", name: "Прочие долгосрочные обязательства"}
|
||||
- {code: "1400", name: "Итого по разделу IV"}
|
||||
- {code: "1510", name: "Заемные средства"}
|
||||
- {code: "1520", name: "Кредиторская задолженность"}
|
||||
- {code: "1540", name: "Оценочные обязательства"}
|
||||
- {code: "1550", name: "Прочие краткосрочные обязательства"}
|
||||
- {code: "1500", name: "Итого по разделу V"}
|
||||
- {code: "1700", name: "БАЛАНС"}
|
||||
INCOME:
|
||||
- {code: "2110", name: "Выручка"}
|
||||
- {code: "2120", name: "Себестоимость продаж"}
|
||||
- {code: "2100", name: "Валовая прибыль (убыток)"}
|
||||
- {code: "2210", name: "Коммерческие расходы"}
|
||||
- {code: "2220", name: "Управленческие расходы"}
|
||||
- {code: "2200", name: "Прибыль (убыток) от продаж"}
|
||||
- {code: "2310", name: "Доходы от участия в других организациях"}
|
||||
- {code: "2320", name: "Проценты к получению"}
|
||||
- {code: "2330", name: "Проценты к уплате"}
|
||||
- {code: "2340", name: "Прочие доходы"}
|
||||
- {code: "2350", name: "Прочие расходы"}
|
||||
- {code: "2300", name: "Прибыль (убыток) от продолжающейся деятельности до налогообложения"}
|
||||
- {code: "2410", name: "Налог на прибыль организаций"}
|
||||
- {code: "2411", name: "текущий налог на прибыль организаций"}
|
||||
- {code: "2412", name: "отложенный налог на прибыль организаций"}
|
||||
- {code: "2420", name: "Прибыль (убыток) от прекращаемой деятельности"}
|
||||
- {code: "2400", name: "Чистая прибыль (убыток)"}
|
||||
- {code: "2510", name: "Результат переоценки внеоборотных активов, не включаемый в чистую прибыль (убыток)"}
|
||||
- {code: "2520", name: "Результат прочих операций, не включаемый в чистую прибыль (убыток)"}
|
||||
- {code: "2530", name: "Налог на прибыль организаций, относящийся к результатам переоценки внеоборотных активов и прочих операций, не включаемых в чистую прибыль (убыток)"}
|
||||
- {code: "2500", name: "Совокупный финансовый результат"}
|
||||
- {code: "2900", name: "Базовая прибыль (убыток) на акцию, руб. коп."}
|
||||
- {code: "2910", name: "Разводненная прибыль (убыток) на акцию, руб. коп."}
|
||||
66
api/src/domain/statement_lines.py
Normal file
66
api/src/domain/statement_lines.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Справочник строк форм ББ/ОФР по редакциям — полный состав формы для экрана проверки (05-domain-rules §7 «Ядро»;
|
||||
change statement-template-lines, design D-2).
|
||||
|
||||
Данные — `api/rules/statement_lines.yaml`; порядок строк в списке — порядок формы. Редакцию выбирает
|
||||
`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: dict[str, dict[str, tuple[TemplateLine, ...]]]
|
||||
|
||||
def lines(self, edition: str | None, form: str) -> tuple[TemplateLine, ...]:
|
||||
"""Строки формы `form` редакции `edition` в порядке формы; без редакции или для чужой формы — пусто."""
|
||||
if edition is None:
|
||||
return ()
|
||||
return self.editions.get(edition, {}).get(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} — не четыре цифры")
|
||||
codes = [line.code for line in lines]
|
||||
if len(codes) != len(set(codes)):
|
||||
raise ValueError(f"редакция {edition}, форма {form}: повтор кода")
|
||||
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
|
||||
72
api/tests/test_domain_statement_lines.py
Normal file
72
api/tests/test_domain_statement_lines.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""Справочник строк форм ББ/ОФР по редакциям (spec documents-api «Форма ББ/ОФР для экрана проверки»; design D-2)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.domain.statement_lines import TemplateLine, load_line_catalog
|
||||
|
||||
CATALOG = load_line_catalog()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("edition", "form", "count", "first", "last"),
|
||||
[
|
||||
("66n", "BALANCE", 37, "1110", "1700"),
|
||||
("66n", "INCOME", 22, "2110", "2910"),
|
||||
("fsbu4", "BALANCE", 34, "1110", "1700"),
|
||||
("fsbu4", "INCOME", 23, "2110", "2910"),
|
||||
],
|
||||
)
|
||||
def test_edition_composition(edition: str, form: str, count: int, first: str, last: str) -> None:
|
||||
lines = CATALOG.lines(edition, form)
|
||||
assert len(lines) == count
|
||||
assert lines[0].code == first and lines[-1].code == last
|
||||
assert len({line.code for line in lines}) == count
|
||||
|
||||
|
||||
def test_names_fixed_from_templates() -> None:
|
||||
names = {line.code: line.name for line in CATALOG.lines("fsbu4", "BALANCE")}
|
||||
assert names["1110"] == "Нематериальные активы"
|
||||
assert names["1210"] == "Запасы"
|
||||
assert names["1310"] == "Уставный капитал (складочный капитал, уставный фонд, вклады товарищей)"
|
||||
assert names["1410"] == names["1510"] == "Заемные средства"
|
||||
assert names["1600"] == names["1700"] == "БАЛАНС"
|
||||
old = {line.code: line.name for line in CATALOG.lines("66n", "BALANCE")}
|
||||
assert old["1160"] == "Доходные вложения в материальные ценности"
|
||||
assert "1105" not in old and "1215" not in old and "1215" in names
|
||||
income = {line.code: line.name for line in CATALOG.lines("fsbu4", "INCOME")}
|
||||
assert "2420" in income and "2460" not in income
|
||||
assert all(
|
||||
"\n" not in line.name for e in ("66n", "fsbu4") for f in ("BALANCE", "INCOME") for line in CATALOG.lines(e, f)
|
||||
)
|
||||
|
||||
|
||||
def test_no_edition_means_no_lines() -> None:
|
||||
assert CATALOG.lines(None, "BALANCE") == ()
|
||||
assert CATALOG.lines("unknown", "BALANCE") == ()
|
||||
assert CATALOG.lines("66n", "OSV") == ()
|
||||
|
||||
|
||||
def test_template_line_is_value_object() -> None:
|
||||
assert TemplateLine("1700", "БАЛАНС") == TemplateLine("1700", "БАЛАНС")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("body", "message"),
|
||||
[
|
||||
("editions:\n - id: x\n forms:\n BALANCE: [{code: '11', name: a}]\n INCOME: []\n", "код"),
|
||||
(
|
||||
"editions:\n - id: x\n forms:\n BALANCE: [{code: '1110', name: a}, {code: '1110', name: b}]\n"
|
||||
" INCOME: []\n",
|
||||
"повтор",
|
||||
),
|
||||
("editions:\n - id: x\n forms:\n BALANCE: []\n", "INCOME"),
|
||||
("editions: [\n", "statement_lines"),
|
||||
],
|
||||
)
|
||||
def test_invalid_catalog_is_value_error(tmp_path: Path, body: str, message: str) -> None:
|
||||
path = tmp_path / "statement_lines.yaml"
|
||||
path.write_text(body, encoding="utf-8")
|
||||
with pytest.raises(ValueError, match=message):
|
||||
load_line_catalog(path)
|
||||
Loading…
x
Reference in New Issue
Block a user