arm-tabular-parser/api/tests/synthetic/generate_statements.py
Andrey Epifancev be5ad8ce86 refactor: удалён собственный конвейер форм и словари форм — шаги 2–8 в arm_statements (#54)
Удалены модули statements/{checks,columns,detect,header,lines,merge,statement,legacy_models}.py, словари
rules/statement_{headers,periods,checks}.yaml и их тесты. Словари форм и шаги 2–8 — в пакете arm_statements.
Rules без полей statement_*; покрытие словарей ОСВ (DICTIONARIES) без словарей форм. Документация сервиса
переписана под адаптер форм, контракт 2.0 и коды OSV_*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 21:53:29 +04:00

229 lines
11 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.

"""Синтетические формы ББ/ОФР (Срез 2b): `python -m tests.synthetic.generate_statements <каталог>` из `api/`.
st01 — ББ и ОФР на одном листе; st02 — «csv-подобный» лист: заголовок формы, коды без подписи «Код», заголовок в две
строки, колонка «Изменение, %», отрицательные в скобках; st03 — форма без колонки кодов; st04 — книга с листом ОСВ
и листом баланса; st05 — одинокая страница 2; st06 — варианты заголовков, шапки и подписей, которых нет в реальных
файлах (покрытие словарей: старое название ОФР, расшифровки, ОДДС, ОИК, целевое использование, «Пояснения»).
st07 — лист-продолжение ОФР с одной кодовой строкой (O-9). Детерминирован (см. `generate.py`).
"""
import argparse
import sys
from collections.abc import Callable, Sequence
from pathlib import Path
from tests.synthetic.generate import ORG, OsvSpec, SheetSpec, base_tree, build_osv, to_xlsx
__all__ = ["FILE_NAMES", "generate_all", "main"]
BALANCE_LINES = [
("Основные средства", "1150", 100, 90),
("Итого по разделу I", "1100", 100, 90),
("Запасы", "1210", 50, 40),
("Итого по разделу II", "1200", 50, 40),
("БАЛАНС", "1600", 150, 130),
("Уставный капитал", "1310", 150, 130),
("Итого по разделу III", "1300", 150, 130),
("БАЛАНС", "1700", 150, 130),
]
INCOME_LINES = [
("Выручка", "2110", 1000, 900),
("Себестоимость продаж", "2120", "(600)", "(500)"),
("Валовая прибыль (убыток)", "2100", 400, 400),
("Коммерческие расходы", "2210", "(50)", "(40)"),
("Управленческие расходы", "2220", "(50)", "(60)"),
("Прибыль (убыток) от продаж", "2200", 300, 300),
("Проценты к уплате", "2330", "(100)", "(100)"),
("Прибыль (убыток) до налогообложения", "2300", 200, 200),
("Налог на прибыль", "2410", "(40)", "(40)"),
("Чистая прибыль (убыток)", "2400", 160, 160),
] # полный набор: все жёсткие соотношения ОФР сходятся; скобки — запись 66н
def _head(sheet: SheetSpec, row: int, title: str, subtitle: str, okud: str) -> int:
sheet.put(row, 2, title)
sheet.put(row + 1, 2, subtitle)
sheet.put(row + 2, 6, "Форма по ОКУД")
sheet.put(row + 2, 7, okud)
sheet.put(row + 3, 1, "Организация")
sheet.put(row + 3, 3, ORG)
sheet.put(row + 3, 6, "ИНН")
sheet.put(row + 3, 7, "7700000000")
sheet.put(row + 4, 1, "Единица измерения:")
sheet.put(row + 4, 3, "тыс. руб.")
return row + 6
def _table(
sheet: SheetSpec, row: int, labels: Sequence[str], lines: Sequence[tuple], *, caption: str | None = "Код"
) -> int:
sheet.put(row, 2, "Наименование показателя")
if caption:
sheet.put(row, 4, caption)
for offset, label in enumerate(labels):
sheet.put(row, 5 + offset, label)
for index, (name, code, *values) in enumerate(lines, start=row + 1):
sheet.put(index, 2, name)
sheet.put(index, 4, code)
for offset, value in enumerate(values):
sheet.put(index, 5 + offset, value)
return row + 1 + len(lines)
def _st01() -> list[SheetSpec]:
sheet = SheetSpec("Отчетность")
row = _head(sheet, 1, "Бухгалтерский баланс", "на 31 декабря 2024 г.", "0710001")
row = _table(sheet, row, ["На 31 декабря 2024 г.", "На 31 декабря 2023 г."], BALANCE_LINES)
row = _head(sheet, row + 1, "Отчет о финансовых результатах", "за 2024 г.", "0710002")
_table(sheet, row, ["За 2024 г.", "За 2023 г."], INCOME_LINES)
return [sheet]
# Значения st02, согласованные со всеми жёсткими соотношениями ББ (словарь пакета arm_statements) в обеих колонках:
# 1100 = 1150, 1200 = 1210 (скобки — запись 66н), 1600 = 1100 + 1200 = 1700, 1300 = 1310, 1700 = 1300.
ST02_VALUES: dict[str, tuple[int | str, int | str]] = {
"1150": (100, 120),
"1100": (100, 120),
"1210": ("(50)", "(40)"),
"1200": ("(50)", "(40)"),
"1600": (50, 80),
"1310": (50, 80),
"1300": (50, 80),
"1700": (50, 80),
}
def _st02() -> list[SheetSpec]:
sheet = SheetSpec("Лист1")
sheet.put(1, 1, "Бухгалтерский баланс")
sheet.put(2, 1, "Организация")
sheet.put(2, 2, 'ООО "Синтетика"')
sheet.put(3, 1, "ИНН")
sheet.put(3, 2, "7700000001")
sheet.put(5, 1, "Показатель")
sheet.put(5, 3, "На 31 декабря")
sheet.put(5, 4, "На 31 декабря")
sheet.put(5, 5, "Изменение, %")
sheet.put(6, 3, "2024 г.")
sheet.put(6, 4, "2023 г.")
for index, (name, code, current, previous) in enumerate(BALANCE_LINES, start=7):
sheet.put(index, 1, name)
sheet.put(index, 2, code)
value_now, value_before = ST02_VALUES.get(code, (current, previous))
sheet.put(index, 3, value_now)
sheet.put(index, 4, value_before)
sheet.put(index, 5, 5)
return [sheet]
def _st03() -> list[SheetSpec]:
sheet = SheetSpec("ББ")
row = _head(sheet, 1, "Бухгалтерский баланс", "на 31 декабря 2024 г.", "0710001")
sheet.put(row, 2, "Показатель")
sheet.put(row, 5, "На 31 декабря 2024 г.")
for index, (name, _, current, _) in enumerate(BALANCE_LINES, start=row + 1):
sheet.put(index, 2, name)
sheet.put(index, 5, current)
return [sheet]
def _st04() -> list[SheetSpec]:
balance = SheetSpec("Баланс")
row = _head(balance, 1, "Бухгалтерский баланс", "на 31 декабря 2024 г.", "0710001")
_table(balance, row, ["На 31 декабря 2024 г."], [(name, code, current) for name, code, current, _ in BALANCE_LINES])
return [build_osv(OsvSpec(), base_tree()), balance]
def _st05() -> list[SheetSpec]:
sheet = SheetSpec("Лист_1")
sheet.put(1, 8, "Форма 0710001 с.2")
_table(sheet, 2, ["На 31 декабря 2024 г.", "На 31 декабря 2023 г."], BALANCE_LINES[5:])
return [sheet]
DISCLOSURE_TITLES = (
"Расшифровка дебиторской задолженности",
"Отчет об изменениях капитала",
"Отчет о движении денежных средств",
"Отчет о целевом использовании средств",
)
ST06_OKUD_ONLY = "0710005" # прочие формы 0710xxx — расшифровки (design D-3, решение №2)
def _st06() -> list[SheetSpec]:
income = SheetSpec("Прибыли")
income.put(1, 2, "Отчет о прибылях и убытках")
income.put(2, 2, "«Синтетика-Групп»") # название без правовой формы и прямых кавычек: найдётся по «
income.put(3, 1, "Организация") # значения правее и ниже нет: название берётся из строки выше
income.put(5, 1, "Адрес")
income.put(5, 3, "г. Москва, ул. Синтетическая, д. 1")
income.put(6, 1, "Единица измерения:")
income.put(6, 3, "тыс. руб.")
header_row = 8
_table(income, header_row, ["31.12.2024", "31.12.2023"], INCOME_LINES)
income.put(header_row, 3, "Пояснения")
# длинный текст в колонке «Пояснения»: без подписи она сошла бы за колонку названий (columns._name_column)
income.put(header_row + 1, 3, "См. подробные пояснения к выручке в составе раздела пятого")
sheets = [income]
for title in DISCLOSURE_TITLES:
sheet = SheetSpec(title[:25])
sheet.put(1, 2, title)
sheets.append(sheet)
okud_only = SheetSpec("Прочая форма")
okud_only.put(1, 2, "Дополнительные сведения")
okud_only.put(2, 6, "Форма по ОКУД")
okud_only.put(2, 7, ST06_OKUD_ONLY)
return [*sheets, okud_only]
def _st07() -> list[SheetSpec]:
"""ОФР без строки 2400 на первом листе и лист-продолжение с единственной кодовой строкой 2400 (O-9, как у НПЗ)."""
first = SheetSpec("ОФР")
row = _head(first, 1, "Отчет о финансовых результатах", "за 2024 г.", "0710002")
_table(first, row, ["За 2024 г.", "За 2023 г."], INCOME_LINES[:-1])
tail = SheetSpec("ОФР_2")
tail.put(1, 2, INCOME_LINES[-1][0])
tail.put(1, 4, INCOME_LINES[-1][1])
tail.put(1, 5, INCOME_LINES[-1][2])
tail.put(1, 6, INCOME_LINES[-1][3])
return [first, tail]
FILES: dict[str, tuple[Callable[[], list[SheetSpec]], Callable[[Sequence[SheetSpec]], bytes]]] = {
"st01_both_forms_one_sheet.xlsx": (_st01, to_xlsx),
"st02_csv_like.xlsx": (_st02, to_xlsx),
"st03_no_codes.xlsx": (_st03, to_xlsx),
"st04_mixed_book.xlsx": (_st04, to_xlsx),
"st05_continuation_only.xlsx": (_st05, to_xlsx),
"st06_header_variants.xlsx": (_st06, to_xlsx),
"st07_single_code_continuation.xlsx": (_st07, to_xlsx),
}
FILE_NAMES = tuple(FILES)
def generate_all(directory: Path) -> list[Path]:
directory.mkdir(parents=True, exist_ok=True)
written: list[Path] = []
for name, (build, serialize) in FILES.items():
path = directory / name
path.write_bytes(serialize(build()))
written.append(path)
return written
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="python -m tests.synthetic.generate_statements", description="Синтетические формы st01…st07."
)
parser.add_argument(
"directory", type=Path, help="каталог назначения, например ../../fixtures/statements-xlsx/synthetic"
)
args = parser.parse_args(argv)
for path in generate_all(args.directory):
print(path)
return 0
if __name__ == "__main__":
sys.exit(main())