I-1: лист с 1–2 кодами после листа формы — продолжение (min_codes_continuation в словаре). M-10: тип формы маркера продолжения запрещает присоединение к документу иного типа. I-2: одиночный месяц не задаёт границы при дате на число/квартале (not_single_month). I-3: название строки из ближайшей текстовой ячейки левее кодов, пустое имя без confidence. M-5: units_thousands и okud_code в словаре, без type: ignore в detect.py. M-8: строка нумерации и нули не считаются строкой без кода с суммами. M-11: предупреждение о дробных значениях — одно на документ. T13.5: _upper_header_row возвращает верхнюю строку при склейке хотя бы одной колонки. M-6/M-7/T15.1/T15.2: остатки statement_markers, описание fractional_share_percent, README, тест воркера. Refs #14 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
229 lines
11 KiB
Python
229 lines
11 KiB
Python
"""Синтетические формы ББ/ОФР (Срез 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, согласованные со всеми жёсткими соотношениями ББ (statement_checks.yaml) в обеих колонках:
|
||
# 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())
|