feat(domain): словарь имён с синонимами (Р-20) и совпадение наименований §11.9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
782580beb4
commit
1dc510fbb5
@ -1,4 +1,6 @@
|
|||||||
# Справочник строк форм ББ/ОФР по редакциям: полный состав формы для экрана проверки (05-domain-rules §7 «Ядро»,
|
# Словарь имён: код → наименования по редакциям, несколько имён на код (Р-20; порядок — contracts/statement-code-orders.yaml).
|
||||||
|
# Первое имя кода — справочное имя и состав формы; остальные — синонимы и законные повторы для совпадения наименований
|
||||||
|
# (§11.9). Справочник строк форм ББ/ОФР по редакциям: полный состав формы для экрана проверки (05-domain-rules §7 «Ядро»,
|
||||||
# design D-2 change statement-template-lines). Порядок строк в списке — порядок формы.
|
# design D-2 change statement-template-lines). Порядок строк в списке — порядок формы.
|
||||||
# Редакция выбирается по дате периода документа (edition_for): 66n — до 2024-12-31 включительно, fsbu4 — позже.
|
# Редакция выбирается по дате периода документа (edition_for): 66n — до 2024-12-31 включительно, fsbu4 — позже.
|
||||||
# Источник обеих редакций — шаблоны банка fixtures/statements-xlsx/templates/ («ББ/ОФР до 01.01.2025», «… после
|
# Источник обеих редакций — шаблоны банка fixtures/statements-xlsx/templates/ («ББ/ОФР до 01.01.2025», «… после
|
||||||
@ -72,7 +74,10 @@ editions:
|
|||||||
- id: fsbu4
|
- id: fsbu4
|
||||||
forms:
|
forms:
|
||||||
BALANCE:
|
BALANCE:
|
||||||
|
- {code: "1105", name: "Гудвил"}
|
||||||
- {code: "1110", name: "Нематериальные активы"}
|
- {code: "1110", name: "Нематериальные активы"}
|
||||||
|
- {code: "1130", name: "Нематериальные поисковые активы"}
|
||||||
|
- {code: "1140", name: "Материальные поисковые активы"}
|
||||||
- {code: "1150", name: "Основные средства"}
|
- {code: "1150", name: "Основные средства"}
|
||||||
- {code: "1160", name: "Инвестиционная недвижимость"}
|
- {code: "1160", name: "Инвестиционная недвижимость"}
|
||||||
- {code: "1170", name: "Финансовые вложения"}
|
- {code: "1170", name: "Финансовые вложения"}
|
||||||
@ -82,6 +87,7 @@ editions:
|
|||||||
- {code: "1210", name: "Запасы"}
|
- {code: "1210", name: "Запасы"}
|
||||||
- {code: "1215", name: "Долгосрочные активы к продаже"}
|
- {code: "1215", name: "Долгосрочные активы к продаже"}
|
||||||
- {code: "1220", name: "Налог на добавленную стоимость по приобретенным ценностям"}
|
- {code: "1220", name: "Налог на добавленную стоимость по приобретенным ценностям"}
|
||||||
|
- {code: "1220", name: "НДС, начисленный по отгрузке"}
|
||||||
- {code: "1230", name: "Дебиторская задолженность"}
|
- {code: "1230", name: "Дебиторская задолженность"}
|
||||||
- {code: "1240", name: "Финансовые вложения (за исключением денежных эквивалентов)"}
|
- {code: "1240", name: "Финансовые вложения (за исключением денежных эквивалентов)"}
|
||||||
- {code: "1250", name: "Денежные средства и денежные эквиваленты"}
|
- {code: "1250", name: "Денежные средства и денежные эквиваленты"}
|
||||||
@ -102,6 +108,8 @@ editions:
|
|||||||
- {code: "1400", name: "Итого по разделу IV"}
|
- {code: "1400", name: "Итого по разделу IV"}
|
||||||
- {code: "1510", name: "Заемные средства"}
|
- {code: "1510", name: "Заемные средства"}
|
||||||
- {code: "1520", name: "Кредиторская задолженность"}
|
- {code: "1520", name: "Кредиторская задолженность"}
|
||||||
|
- {code: "1520", name: "Обязательство по аренде"}
|
||||||
|
- {code: "1530", name: "Доходы будущих периодов"}
|
||||||
- {code: "1540", name: "Оценочные обязательства"}
|
- {code: "1540", name: "Оценочные обязательства"}
|
||||||
- {code: "1550", name: "Прочие краткосрочные обязательства"}
|
- {code: "1550", name: "Прочие краткосрочные обязательства"}
|
||||||
- {code: "1500", name: "Итого по разделу V"}
|
- {code: "1500", name: "Итого по разделу V"}
|
||||||
@ -122,7 +130,9 @@ editions:
|
|||||||
- {code: "2410", name: "Налог на прибыль организаций"}
|
- {code: "2410", name: "Налог на прибыль организаций"}
|
||||||
- {code: "2411", name: "текущий налог на прибыль организаций"}
|
- {code: "2411", name: "текущий налог на прибыль организаций"}
|
||||||
- {code: "2412", name: "отложенный налог на прибыль организаций"}
|
- {code: "2412", name: "отложенный налог на прибыль организаций"}
|
||||||
|
- {code: "2410", name: "Налог при упрощенной системе налогообложения"}
|
||||||
- {code: "2420", name: "Прибыль (убыток) от прекращаемой деятельности"}
|
- {code: "2420", name: "Прибыль (убыток) от прекращаемой деятельности"}
|
||||||
|
- {code: "2460", name: "Прочее"}
|
||||||
- {code: "2400", name: "Чистая прибыль (убыток)"}
|
- {code: "2400", name: "Чистая прибыль (убыток)"}
|
||||||
- {code: "2510", name: "Результат переоценки внеоборотных активов, не включаемый в чистую прибыль (убыток)"}
|
- {code: "2510", name: "Результат переоценки внеоборотных активов, не включаемый в чистую прибыль (убыток)"}
|
||||||
- {code: "2520", name: "Результат прочих операций, не включаемый в чистую прибыль (убыток)"}
|
- {code: "2520", name: "Результат прочих операций, не включаемый в чистую прибыль (убыток)"}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
"""Справочник строк форм ББ/ОФР по редакциям — полный состав формы для экрана проверки (05-domain-rules §7 «Ядро»;
|
"""Словарь имён строк форм ББ/ОФР по редакциям — состав формы для экрана проверки (05-domain-rules §7 «Ядро»;
|
||||||
change statement-template-lines, design D-2).
|
change statement-template-lines, design D-2) и справочные имена для совпадения наименований (§11.9, Р-20).
|
||||||
|
|
||||||
Данные — `api/rules/statement_lines.yaml`; порядок строк в списке — порядок формы. Редакцию выбирает
|
Данные — `api/rules/statement_lines.yaml`; порядок строк в списке — порядок формы. Код в редакции может стоять
|
||||||
`statement_mapping.edition_for`; здесь — только состав. Справочник ничего не убирает и не переименовывает в форме:
|
несколько раз (синонимы и законные повторы, Р-20): состав формы берёт первое имя, сравнение наименований — любое.
|
||||||
строки источника, которых в нём нет, остаются как есть.
|
Редакцию выбирает `statement_mapping.edition_for`; здесь — только состав. Справочник ничего не убирает и не
|
||||||
|
переименовывает в форме: строки источника, которых в нём нет, остаются как есть.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
@ -28,14 +29,46 @@ class TemplateLine:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class LineCatalog:
|
class LineCatalog:
|
||||||
|
"""`editions[редакция][форма]` — все записи словаря в порядке формы, повтор кода допустим."""
|
||||||
|
|
||||||
editions: dict[str, dict[str, tuple[TemplateLine, ...]]]
|
editions: dict[str, dict[str, tuple[TemplateLine, ...]]]
|
||||||
|
|
||||||
def lines(self, edition: str | None, form: str) -> tuple[TemplateLine, ...]:
|
def _entries(self, edition: str | None, form: str) -> tuple[TemplateLine, ...]:
|
||||||
"""Строки формы `form` редакции `edition` в порядке формы; без редакции или для чужой формы — пусто."""
|
|
||||||
if edition is None:
|
if edition is None:
|
||||||
return ()
|
return ()
|
||||||
return self.editions.get(edition, {}).get(form, ())
|
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, ...]:
|
def _form_lines(edition: str, form: str, items: object) -> tuple[TemplateLine, ...]:
|
||||||
if not isinstance(items, list):
|
if not isinstance(items, list):
|
||||||
@ -44,9 +77,6 @@ def _form_lines(edition: str, form: str, items: object) -> tuple[TemplateLine, .
|
|||||||
for line in lines:
|
for line in lines:
|
||||||
if not _CODE.fullmatch(line.code):
|
if not _CODE.fullmatch(line.code):
|
||||||
raise ValueError(f"редакция {edition}, форма {form}: код {line.code!r} — не четыре цифры")
|
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
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
51
api/src/domain/statement_names.py
Normal file
51
api/src/domain/statement_names.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""Совпадение наименований строки формы со справочными (05-domain-rules §11.9, спека «Совпадение наименований»)."""
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
import re
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
__all__ = ["names_match", "normalize", "section_number"]
|
||||||
|
|
||||||
|
_NON_ALNUM = re.compile(r"[\W_]+")
|
||||||
|
_TOTAL_PREFIX = "итого по разделу"
|
||||||
|
_MIN_CONTAINED = 5
|
||||||
|
_MIN_RATIO = 0.8
|
||||||
|
_ROMAN_LATIN = str.maketrans({"і": "i", "х": "x"}) # кириллические «і», «х» -> латиница
|
||||||
|
_ROMAN_HEAD = re.compile(r"[ivx]+")
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(text: str) -> str:
|
||||||
|
"""Нижний регистр, `ё` -> `е`, всё кроме букв и цифр — пробел, схлопывание, `strip`."""
|
||||||
|
return _NON_ALNUM.sub(" ", text.lower().replace("ё", "е")).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def section_number(normalized: str) -> str | None:
|
||||||
|
"""Номер раздела «Итого по разделу …» (строчными буквами i, v, x) из нормализованного имени; остаток слова —
|
||||||
|
хвост OCR («v1» -> «v») — отбрасывается. Имя не «итого по разделу» или номер не выделен — `None`."""
|
||||||
|
if not normalized.startswith(_TOTAL_PREFIX):
|
||||||
|
return None
|
||||||
|
words = normalized[len(_TOTAL_PREFIX) :].split()
|
||||||
|
if not words:
|
||||||
|
return None
|
||||||
|
head = _ROMAN_HEAD.match(words[0].translate(_ROMAN_LATIN))
|
||||||
|
return head.group() if head else None
|
||||||
|
|
||||||
|
|
||||||
|
def _match_pair(a: str, b: str) -> bool:
|
||||||
|
if a.startswith(_TOTAL_PREFIX) or b.startswith(_TOTAL_PREFIX):
|
||||||
|
na, nb = section_number(a), section_number(b)
|
||||||
|
return a == b if na is None or nb is None else na == nb
|
||||||
|
if a == b:
|
||||||
|
return True
|
||||||
|
short, long_ = sorted((a, b), key=len)
|
||||||
|
if len(short) >= _MIN_CONTAINED and short in long_:
|
||||||
|
return True
|
||||||
|
return difflib.SequenceMatcher(None, a, b).ratio() >= _MIN_RATIO
|
||||||
|
|
||||||
|
|
||||||
|
def names_match(recognized: str | None, references: Iterable[str]) -> bool:
|
||||||
|
"""Наименование совпало хотя бы с одним справочным; пустое имя или нет справочных — `False`."""
|
||||||
|
name = normalize(recognized or "")
|
||||||
|
if not name:
|
||||||
|
return False
|
||||||
|
return any(ref and _match_pair(name, ref) for ref in map(normalize, references))
|
||||||
@ -140,7 +140,7 @@ class StatementService:
|
|||||||
columns, lines = await repo.load_form(self._session, meta.id)
|
columns, lines = await repo.load_form(self._session, meta.id)
|
||||||
report, cross, parents = await self.checks_report(document, meta)
|
report, cross, parents = await self.checks_report(document, meta)
|
||||||
edition = edition_for(document.reporting_date)
|
edition = edition_for(document.reporting_date)
|
||||||
template = load_line_catalog().lines(edition, document.document_type.value)
|
template = load_line_catalog().template_lines(edition, document.document_type.value)
|
||||||
return schemas.statement_out(
|
return schemas.statement_out(
|
||||||
document, meta, columns, lines, report, cross, edition=edition, template=template, parents=parents
|
document, meta, columns, lines, report, cross, edition=edition, template=template, parents=parents
|
||||||
)
|
)
|
||||||
@ -222,10 +222,9 @@ class StatementService:
|
|||||||
lines = await repo.form_lines(self._session, meta.id)
|
lines = await repo.form_lines(self._session, meta.id)
|
||||||
by_id = {line.id: line for line in lines}
|
by_id = {line.id: line for line in lines}
|
||||||
codes = {line.line_code for line in lines}
|
codes = {line.line_code for line in lines}
|
||||||
template = {
|
edition = edition_for(document.reporting_date)
|
||||||
item.code: item
|
catalog_lines = load_line_catalog().template_lines(edition, document.document_type.value)
|
||||||
for item in load_line_catalog().lines(edition_for(document.reporting_date), document.document_type.value)
|
template = {item.code: item for item in catalog_lines}
|
||||||
}
|
|
||||||
targets: list[tuple[schemas.CellPatch, FinancialStatementLine | None, TemplateLine | None]] = []
|
targets: list[tuple[schemas.CellPatch, FinancialStatementLine | None, TemplateLine | None]] = []
|
||||||
for cell in body.values:
|
for cell in body.values:
|
||||||
if cell.column_id not in columns:
|
if cell.column_id not in columns:
|
||||||
|
|||||||
@ -44,7 +44,7 @@ MEZH_BALANCE = GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма
|
|||||||
UNAGRANDE_BALANCE = GOLDEN_STATEMENTS / "unagrande-group" / "2025-Q3" / "ББ. Лист 1 3кв.25.json"
|
UNAGRANDE_BALANCE = GOLDEN_STATEMENTS / "unagrande-group" / "2025-Q3" / "ББ. Лист 1 3кв.25.json"
|
||||||
MEZH_INCOME = GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма2_бух_09.json"
|
MEZH_INCOME = GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма2_бух_09.json"
|
||||||
MEZH_BALANCE_LINES = 37
|
MEZH_BALANCE_LINES = 37
|
||||||
MEZH_BALANCE_TEMPLATE_ONLY = {"1215"} # коды fsbu4, которых нет в файле Межениновской (файл по форме 66н)
|
MEZH_BALANCE_TEMPLATE_ONLY = {"1105", "1215"} # коды fsbu4, которых нет в файле Межениновской (файл по форме 66н)
|
||||||
MEZH_INCOME_TEMPLATE_ONLY = {"2420"}
|
MEZH_INCOME_TEMPLATE_ONLY = {"2420"}
|
||||||
EVENTS_AFTER_BREAK_AND_ACCEPT = 2
|
EVENTS_AFTER_BREAK_AND_ACCEPT = 2
|
||||||
|
|
||||||
@ -1222,7 +1222,7 @@ async def test_patch_line_code_existing_or_unknown_rejected(
|
|||||||
client = await auth_client(KA_RF)
|
client = await auth_client(KA_RF)
|
||||||
col0 = await _column(client, document.id, 0)
|
col0 = await _column(client, document.id, 0)
|
||||||
count = await db_line_count(seeded, document)
|
count = await db_line_count(seeded, document)
|
||||||
for code in ("1150", "1105"): # 1150 есть в форме; 1105 нет в справочнике fsbu4
|
for code in ("1150", "1106"): # 1150 есть в форме; 1106 нет в справочнике fsbu4
|
||||||
response = await client.patch(
|
response = await client.patch(
|
||||||
f"/documents/{document.id}/statement",
|
f"/documents/{document.id}/statement",
|
||||||
json={"values": [{"line_code": code, "column_id": col0, "amount": "1"}]},
|
json={"values": [{"line_code": code, "column_id": col0, "amount": "1"}]},
|
||||||
|
|||||||
@ -14,38 +14,74 @@ CATALOG = load_line_catalog()
|
|||||||
[
|
[
|
||||||
("66n", "BALANCE", 37, "1110", "1700"),
|
("66n", "BALANCE", 37, "1110", "1700"),
|
||||||
("66n", "INCOME", 22, "2110", "2910"),
|
("66n", "INCOME", 22, "2110", "2910"),
|
||||||
("fsbu4", "BALANCE", 34, "1110", "1700"),
|
("fsbu4", "BALANCE", 38, "1105", "1700"),
|
||||||
("fsbu4", "INCOME", 23, "2110", "2910"),
|
("fsbu4", "INCOME", 24, "2110", "2910"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_edition_composition(edition: str, form: str, count: int, first: str, last: str) -> None:
|
def test_edition_composition(edition: str, form: str, count: int, first: str, last: str) -> None:
|
||||||
lines = CATALOG.lines(edition, form)
|
lines = CATALOG.template_lines(edition, form)
|
||||||
assert len(lines) == count
|
assert len(lines) == count
|
||||||
assert lines[0].code == first and lines[-1].code == last
|
assert lines[0].code == first and lines[-1].code == last
|
||||||
assert len({line.code for line in lines}) == count
|
assert len({line.code for line in lines}) == count
|
||||||
|
|
||||||
|
|
||||||
def test_names_fixed_from_templates() -> None:
|
def test_names_fixed_from_templates() -> None:
|
||||||
names = {line.code: line.name for line in CATALOG.lines("fsbu4", "BALANCE")}
|
names = {line.code: line.name for line in CATALOG.template_lines("fsbu4", "BALANCE")}
|
||||||
assert names["1110"] == "Нематериальные активы"
|
assert names["1110"] == "Нематериальные активы"
|
||||||
assert names["1210"] == "Запасы"
|
assert names["1210"] == "Запасы"
|
||||||
assert names["1310"] == "Уставный капитал (складочный капитал, уставный фонд, вклады товарищей)"
|
assert names["1310"] == "Уставный капитал (складочный капитал, уставный фонд, вклады товарищей)"
|
||||||
assert names["1410"] == names["1510"] == "Заемные средства"
|
assert names["1410"] == names["1510"] == "Заемные средства"
|
||||||
assert names["1600"] == names["1700"] == "БАЛАНС"
|
assert names["1600"] == names["1700"] == "БАЛАНС"
|
||||||
old = {line.code: line.name for line in CATALOG.lines("66n", "BALANCE")}
|
old = {line.code: line.name for line in CATALOG.template_lines("66n", "BALANCE")}
|
||||||
assert old["1160"] == "Доходные вложения в материальные ценности"
|
assert old["1160"] == "Доходные вложения в материальные ценности"
|
||||||
assert "1105" not in old and "1215" not in old and "1215" in names
|
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")}
|
income = {line.code: line.name for line in CATALOG.template_lines("fsbu4", "INCOME")}
|
||||||
assert "2420" in income and "2460" not in income
|
assert "2420" in income and "2460" in income
|
||||||
assert all(
|
assert all(
|
||||||
"\n" not in line.name for e in ("66n", "fsbu4") for f in ("BALANCE", "INCOME") for line in CATALOG.lines(e, f)
|
"\n" not in line.name
|
||||||
|
for e in ("66n", "fsbu4")
|
||||||
|
for f in ("BALANCE", "INCOME")
|
||||||
|
for line in CATALOG.template_lines(e, f)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_no_edition_means_no_lines() -> None:
|
def test_no_edition_means_no_lines() -> None:
|
||||||
assert CATALOG.lines(None, "BALANCE") == ()
|
assert CATALOG.template_lines(None, "BALANCE") == ()
|
||||||
assert CATALOG.lines("unknown", "BALANCE") == ()
|
assert CATALOG.template_lines("unknown", "BALANCE") == ()
|
||||||
assert CATALOG.lines("66n", "OSV") == ()
|
assert CATALOG.template_lines("66n", "OSV") == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalog_two_names_one_template_line(tmp_path: Path) -> None:
|
||||||
|
lines = CATALOG.template_lines("fsbu4", "BALANCE")
|
||||||
|
assert [line.code for line in lines].count("1520") == 1
|
||||||
|
assert {line.code: line.name for line in lines}["1520"] == "Кредиторская задолженность"
|
||||||
|
assert CATALOG.names("BALANCE", "1520") == ("Кредиторская задолженность", "Обязательство по аренде")
|
||||||
|
assert CATALOG.reference_name("fsbu4", "BALANCE", "1520") == "Кредиторская задолженность"
|
||||||
|
assert CATALOG.reference_name("fsbu4", "BALANCE", "9999") is None
|
||||||
|
assert "1520" in CATALOG.edition_codes("fsbu4", "BALANCE") and "9999" not in CATALOG.edition_codes("66n", "INCOME")
|
||||||
|
path = tmp_path / "statement_lines.yaml"
|
||||||
|
path.write_text(
|
||||||
|
"editions:\n - id: x\n forms:\n BALANCE: [{code: '1110', name: a}, {code: '1110', name: b}]\n"
|
||||||
|
" INCOME: []\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
own = load_line_catalog(path)
|
||||||
|
assert [line.name for line in own.template_lines("x", "BALANCE")] == ["a"]
|
||||||
|
assert own.names("BALANCE", "1110") == ("a", "b")
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalog_fsbu4_additions() -> None:
|
||||||
|
assert "НДС, начисленный по отгрузке" in CATALOG.names("BALANCE", "1220")
|
||||||
|
assert "Налог при упрощенной системе налогообложения" in CATALOG.names("INCOME", "2410")
|
||||||
|
expected = {
|
||||||
|
"1105": "Гудвил",
|
||||||
|
"1130": "Нематериальные поисковые активы",
|
||||||
|
"1140": "Материальные поисковые активы",
|
||||||
|
"1530": "Доходы будущих периодов",
|
||||||
|
}
|
||||||
|
for code, name in expected.items():
|
||||||
|
assert CATALOG.reference_name("fsbu4", "BALANCE", code) == name
|
||||||
|
assert CATALOG.reference_name("fsbu4", "INCOME", "2460") == "Прочее"
|
||||||
|
|
||||||
|
|
||||||
def test_template_line_is_value_object() -> None:
|
def test_template_line_is_value_object() -> None:
|
||||||
@ -56,11 +92,6 @@ def test_template_line_is_value_object() -> None:
|
|||||||
("body", "message"),
|
("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: '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 - id: x\n forms:\n BALANCE: []\n", "INCOME"),
|
||||||
("editions: [\n", "statement_lines"),
|
("editions: [\n", "statement_lines"),
|
||||||
],
|
],
|
||||||
|
|||||||
40
api/tests/test_domain_statement_names.py
Normal file
40
api/tests/test_domain_statement_names.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""Совпадение наименований §11.9 (спека «Совпадение наименований»)."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.domain.statement_names import names_match, normalize, section_number
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("recognized", "references", "expected"),
|
||||||
|
[
|
||||||
|
("Нематериальные активы", ["Результаты исследований и разработок"], False),
|
||||||
|
(
|
||||||
|
"НДС, начисленный по отгрузке",
|
||||||
|
["Налог на добавленную стоимость по приобретенным ценностям", "НДС, начисленный по отгрузке"],
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
("Итого по разделу I", ["Итого по разделу II"], False),
|
||||||
|
("Итого по разделу V1", ["Итого по разделу V"], True),
|
||||||
|
("Итого по разделу V /", ["Итого по разделу V"], True),
|
||||||
|
("Итого по разделу", ["Итого по разделу II"], False),
|
||||||
|
("АКТИВ I. Внеоборотные активы Гудвил", ["Гудвил"], True),
|
||||||
|
("Расчёты", ["Расчеты"], True),
|
||||||
|
("", ["Запасы"], False),
|
||||||
|
(None, ["Запасы"], False),
|
||||||
|
("Запасы", [], False),
|
||||||
|
("Итого по разделу х", ["Итого по разделу X"], True),
|
||||||
|
("Итого по разделу", ["Итого по разделу"], True),
|
||||||
|
("Итого по разделу II", ["Итого по разделу IV"], False),
|
||||||
|
("Запасы", ["Основные средства"], False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_names_match(recognized: str | None, references: list[str], expected: bool) -> None:
|
||||||
|
assert names_match(recognized, references) is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_and_section_number() -> None:
|
||||||
|
assert normalize(" Расчёты,\tс (покупателями)_ ") == "расчеты с покупателями"
|
||||||
|
assert section_number(normalize("Итого по разделу VІ")) == "vi"
|
||||||
|
assert section_number("запасы") is None
|
||||||
|
assert section_number("итого по разделу") is None
|
||||||
Loading…
x
Reference in New Issue
Block a user