39 KiB
Настраиваемый генератор синтетики ГСК — план реализации
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: пакет synthgen/ в репозитории fixtures, который по настройкам генерирует согласованный набор одной ГК —
ОСВ, ББ, ОФР в виде выгрузок 1С и файл состава ГК для импорта ЕЦП.
Architecture: у каждой компании группы — книга проводок (Ledger, двойная запись). Экономика и операции внутри
группы пишут проводки в книги; ОСВ, ББ и ОФР — чистые свёртки книг на даты периодов; запись файлов — по шаблонам,
собранным из реальных выгрузок 1С. Вся случайность — из rng_for(seed, *ключи), поэтому набор детерминирован.
Tech Stack: Python 3.11, openpyxl 3.1.5, PyYAML 6.0.2, pytest 8.3.3; запуск через uv run.
Spec: docs/superpowers/specs/2026-11-12-synthgen-design.md (в этом worktree). Исполнитель читает спеку целиком;
номера § ниже — её разделы, «решение N» — её таблица решений.
Global Constraints
- Рабочий каталог:
/Users/andrey.epifancev/projects/awp-ca/.wt/synthgen/fixtures(веткаfeat/38-synthgen). Первой командой —cdтуда. В основные checkout'ы (awp-ca/fixtures,awp-ca/tabular-parser, …) не делатьcd: хук guard-main блокирует запись, если cwd — репозиторий на main; читать их файлы абсолютными путями. - Python:
UVRUN="uv run -q --with openpyxl==3.1.5 --with pyyaml==6.0.2 --with pytest==8.3.3", запуск из корня worktree:$UVRUN python -m synthgen …,$UVRUN python -m pytest synthgen/tests -q.timeoutна macOS нет. - Код — только в
synthgen/; плюс.gitignore(строкаout/). Сгенерированные наборы не коммитить. - Деньги —
Decimal,ROUND_HALF_UP;floatтолько при записи числа в ячейку xlsx. - Случайность — только через
synthgen.rng.rng_for(seed, *keys);random.random()/модульныйrandom— запрещены. Никакихdatetime.now(),set-итераций без сортировки,hash()строк (рандомизирован в Python). - ИНН — регион
00, верная контрольная сумма; словарь — номера0100001–0109999; компании группы —1000000 + 100 × h(prefix) + i(§4.2, решение 10). - Источник копируемых модулей #35:
/Users/andrey.epifancev/projects/awp-ca/.wt/synthetic-group/fixtures/synthetic-group/gen/(inn.py,xlsx.py,ledger.py,chart.py,forms.py,ecp.py,economy.py). Копия — с docstring «Источник: synthetic-group/gen/<файл> (#35)», без импортов изsynthetic-group/. - Образцы 1С (только чтение): ОСВ —
/Users/andrey.epifancev/projects/awp-ca/fixtures/osv/rb/2024-Q1/сч 60_ 1 кв 2024 г.xlsx; ББ —/Users/andrey.epifancev/projects/awp-ca/fixtures/statements-xlsx/mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx; ОФР —…/1_Форма2_бух_09.xlsx. - Комментарии, docstring, README, сообщения коммитов — по-русски; идентификаторы — английские.
- Трейлер коммита:
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>(или фактическая модель исполнителя). - Не делать push/PR. Не коммитить
.superpowers/. Хук/классификатор заблокировал — не обходить, остановиться (BLOCKED). - Решение по реализации без владельца — строкой в таблицу «Decisions made without discussion» спеки (следующий номер 13, дата, решение, обоснование, цена ошибки) в том же коммите.
Review Focus
group_sizeу границы (2 и 40) иno_inn_companies = group_size— генерация не падает, у каждой компании хотя бы одна пара ВГО или явная запись в README, что пар нет. → тест в Task 7 (test_edge_configs).start=…-Q4иperiods, переходящие через год (2025-Q3, 4 периода) — реформация 99→84 и входящие остатки следующего года, ББ на 31.12 одного года = «на 31 декабря» следующего года. → тест в Task 3 (test_year_rollover).osv_counterparties_mean=0— ОСВ 60/62/76 содержат только компании группы (или пустые, но валидные с «Итого»), парсер их принимает. → тест в Task 5 (test_osv_without_externals).- Префикс с кириллицей или цифрой в начале (
ДЕМО,2GK) — по §3 допустимы буквы/цифры; имя файла ЕЦП и папки строятся без ошибок. → тест в Task 2 (test_prefix_cyrillic_and_digits). - Повторный прогон в существующий непустой
out— старые файлы не смешиваются с новыми. Ожидание: ошибка «каталог не пуст» без--force, с--forceкаталог очищается. → тест в Task 7 (test_out_not_empty).
Task 1: Каркас, настройки, копии модулей #35
Files:
- Create:
synthgen/__init__.py,synthgen/__main__.py,synthgen/config.py,synthgen/rng.py,synthgen/examples/default.yaml,synthgen/inn.py,synthgen/xlsx.py,synthgen/ledger.py,synthgen/chart.py - Modify:
.gitignore(+out/) - Test:
synthgen/tests/conftest.py,synthgen/tests/test_config.py,synthgen/tests/test_inn.py,synthgen/tests/test_ledger.py
Interfaces:
-
Produces:
config.Config— frozen dataclass:group_size: int = 5,prefix: str = "SYN",periods: int = 4,start: str = "2025-Q1",osv_counterparties_mean: int = 60,no_inn_companies: int = 1,vgo_mismatch_share: Decimal = Decimal("0.1"),seed: int = 1,out: Path | None = None.config.ConfigError(ValueError);config.load_config(path: Path | None, overrides: list[str]) -> Config(overrides— строкиключ=значение; YAML и overrides приводятся к типам полей; проверки — диапазоны §3).config.dump_config(cfg: Config) -> str— YAML с ключами в порядке полей.rng.rng_for(seed: int, *keys: object) -> random.Random— зерно = первые 8 байт SHA-256 отrepr((seed, *keys));rng.stable_hash(text: str, modulo: int) -> int— SHA-256 →int % modulo.- Копии #35:
inn.make_inn10(seq),inn.make_inn12(seq),inn.is_valid(inn);xlsx.save_deterministic(wb, path);ledger.An,ledger.Posting,ledger.Ledger(post,balance,turnover),ledger.opening(...);chart.ACCOUNTS,chart.parent(code),chart.is_off_balance(code)—chart.ACCOUNTSдополнить80.09,84,91.09,99,90.09(если нет), сверив коды и названия с/Users/andrey.epifancev/projects/awp-ca/arm-core/api/src/seed/chart_of_accounts.csv(путь найтиfind, если иной). __main__.main(argv: list[str] | None = None) -> int— пока: разбор--config,--set(повторяемый),--out,--force; печатаетdump_configи возвращает 0. Ошибка конфигурации → сообщение в stderr, код 2.
-
Step 1: Тесты.
test_defaults(load_config(None, [])==Config());test_yaml_and_set_override(YAMLgroup_size: 7, overridegroup_size=9→ 9;vgo_mismatch_share=0.25→Decimal("0.25"));test_validation— параметризованный:group_size=1,group_size=41,periods=0,periods=13,start=2025-Q5,start=2019-Q4,prefix=A,prefix="A B",prefix=ABCDEFGHIJKLM,osv_counterparties_mean=151,no_inn_companies=6приgroup_size=5,vgo_mismatch_share=1.5,unknown=1→ConfigError;test_cli_bad_config_exit_2;test_rng_for_stable(rng_for(1,"a").random()дважды одинаково,rng_for(1,"a")≠rng_for(1,"b")); копии тестов #35 дляinnиledger(tests/test_inn.py,tests/test_ledger.pyиз источника, импортыgen.→synthgen.).conftest.pyрегистрирует маркерыparser(приёмка парсером, без окружения — skip) иslow(по умолчанию пропускается, запуск-m slow). -
Step 2: прогнать — FAIL (нет модулей).
-
Step 3: реализовать по Interfaces;
examples/default.yaml— все ключи §3 со значениями по умолчанию и русскими комментариями. -
Step 4:
$UVRUN python -m pytest synthgen/tests -q— PASS. -
Step 5: Коммит
synthgen: каркас, настройки, копии модулей #35 (#38).
Task 2: Словарь, состав группы, выгрузка ЕЦП
Files:
- Create:
synthgen/dictionary/build.py,synthgen/dictionary/companies.csv,synthgen/dictionary.py,synthgen/group.py,synthgen/ecp.py - Test:
synthgen/tests/test_dictionary.py,synthgen/tests/test_group.py,synthgen/tests/test_ecp.py
Interfaces:
-
Consumes:
inn.*,rng.*,config.Config,xlsx.save_deterministic. -
Produces:
dictionary.Org— frozen:name: str(как в 1С:Северная Нива ООО,Аверина Е.И. ИП),form: str(ООО|АО|ПАО|ИП),inn: str,kpp: str(пусто у ИП),kind: str;dictionary.load() -> list[Org](из CSV, порядок файла).group.Member— frozen:code: str(C01…),org: Org,name: str(АО "<prefix> <корень>"),name_1c: str(<prefix> <корень> АО— как 1С пишет контрагента),inn: str,kpp: str,is_head: bool,head_share: Decimal | None(доля головной, у головнойNone),minority: Org | None.group.Group— frozen:prefix,name(ГК <prefix>),gk_id(<prefix>-000001),members: list[Member],no_inn: frozenset[str](коды),variant_name: dict[str, str](код ОСВ-компании без ИНН → код компании группы, записанной другим написанием),externals: list[Org](без банков и без выбранных в группу),banks: list[Org].group.build_group(cfg: Config, orgs: list[Org]) -> Group;group.member_inn(prefix: str, i: int) -> str.ecp.write_ecp(group: Group, path: Path) -> None.
-
Step 1:
build.py— детерминированный (без rng: перебор комбинаций из списков в файле) генератор 400 записей: 30 банков (kind=банк, АО/ПАО, «… Банк»), ~40 ИП (ФИО + «ИП»), остальные ООО/АО по 6 отраслям §4.1. Номера ИНН — подряд от0100001.python -m synthgen.dictionary.buildперезаписываетcompanies.csv(;, UTF-8, заголовокname;form;inn;kpp;kind). CSV закоммитить. -
Step 2: Тесты словаря:
test_dictionary_size_and_kinds(400 записей, 30банк, 30–50 ИП, имена уникальны без учёта регистра);test_dictionary_inn(всеis_valid, длина 10 у ЮЛ / 12 у ИП, номер в0100001–0109999, КПП 9 цифр у ЮЛ);test_dictionary_not_real— ни одно имя (без формы, без регистра) не встречается в названиях организаций/Users/andrey.epifancev/projects/awp-ca/fixtures/directories/ecp-export-russkiy-biskvit.xlsxи в колонке A первых листов файловfixtures/osv/rb/2024-Q1/*.xlsx;test_build_is_reproducible(повторныйbuildвtmp_pathпобайтно равен закоммиченному CSV). -
Step 3: Тесты группы:
test_group_size_and_head(group_size=5→ 5 членов,C01— головная АО, остальные ООО/АО, никто не банк и не ИП);test_member_inn_range(member_inn("SYN", 3)валиден, номер =1000000 + 100 × stable_hash("SYN", 90000) + 3);test_prefixes_do_not_collide(ИНН группSYNиDEMO2по 5 членов не пересекаются; ни один ИНН группы не в словаре);test_externals_exclude_group_and_banks;test_no_inn_selection(no_inn_companies=2→ 2 кода, головная не выбирается, пока есть дочерние; приno_inn_companies=group_size— все);test_variant_name— для каждого кода изno_innесть ровно один другой член группы с вариантным написанием, вариант ≠name_1cи ≠name;test_shares(head_share∈ [0.51, 1.00], у 1–2 дочернихminorityнеNoneи доля < 1, приgroup_size=2— у дочерней может не быть миноритария);test_prefix_cyrillic_and_digits(ДЕМО,2GK—build_groupиwrite_ecpбез ошибок);test_group_deterministic. -
Step 4: Тест ЕЦП:
test_ecp_format— листВыписки, заголовок ровно["ИНН","КПП","iD ГК","Базовый РФ ГК в CRM","Группа компаний","Организация","ГСК","ГСЗ","Тип"], строк =group_size, колонка ГСК —Входит в ГСКу всех, ИНН/КПП текстом (number_format == "@"),Группа компаний=ГК <prefix>. Остальные колонки (Базовый РФ ГК в CRM,Тип) — как вecp.py#35; перед реализацией сверить с форматом импорта ядра (/Users/andrey.epifancev/projects/awp-ca/arm-core/api/src/— поискВходит в ГСК), расхождение — решение в спеку. -
Step 5: FAIL → реализация → PASS (
$UVRUN python -m pytest synthgen/tests -q). -
Step 6: Коммит
synthgen: словарь компаний, состав группы, выгрузка ЕЦП (#38).
Task 3: Периоды и экономика компаний с внешними контрагентами
Files:
- Create:
synthgen/periods.py,synthgen/counterparties.py,synthgen/economy.py - Test:
synthgen/tests/test_periods.py,synthgen/tests/test_counterparties.py,synthgen/tests/test_economy.py
Interfaces:
-
Consumes:
Config,Group,Ledger,rng_for. -
Produces:
periods.Period— frozen:key(2025-Q1…2025-FY),year,quarter,start: date(1 января),end: date,label_1c(1 квартал 2025 г.,1 полугодие 2025 г.,9 месяцев 2025 г.,2025 г.).periods.build_periods(start: str, n: int) -> list[Period];periods.book_start(ps) -> date(31.12 года перед первым периодом — дата входящих остатков);periods.book_end(ps) -> date.counterparties.OSV_ACCOUNTS = ("01","58","60","62","66","67","76","80","90");counterparties.WEIGHTS = {"60": 1.6, "62": 1.6, "76": 1.0, "66": 0.1, "67": 0.05, "58": 0.1};counterparties.plan(cfg: Config, group: Group) -> CounterpartyPlan—CounterpartyPlan.by_file: dict[tuple[str, str], list[Org]](ключ — (код компании, корень счёта)),capped: bool.economy.build_ledgers(cfg: Config, group: Group, ps: list[Period], plan: CounterpartyPlan) -> dict[str, Ledger]— книги всех членов без операций внутри группы (их добавляет Task 4 до закрытия месяцев, см. ниже).- Порядок построения, на который опирается Task 4:
economy.open_books(...) -> dict[str, Ledger](входящие остатки),economy.post_month(ledgers, cfg, group, plan, year, month)(внешние операции месяца),economy.close_month(ledgers, year, month)(закрытие 26/44 → 90, 90/91 → 99, налог, в декабре 99 → 84);build_ledgers=open_books+ цикл месяцев (post_month, затемhooksиз аргументаextra: Callable[[dict[str, Ledger], int, int], None] | None = None, затемclose_month). - Аналитика контрагента в книге:
An(cp=<ИНН Org>)для внешних,An(cp=<код члена>)для компаний группы.
-
Step 1: Тесты периодов:
test_build_periods_simple(2025-Q1, 4 → ключи2025-Q1, 2025-Q2, 2025-Q3, 2025-FY,label_1c1 квартал 2025 г.…2025 г.,startвсегда 1 января);test_build_periods_rollover(2025-Q3, 4 →2025-Q3, 2025-FY, 2026-Q1, 2026-Q2);test_book_start(= 31.12.2024 для обоих). -
Step 2: Тесты контрагентов:
test_mean_matches(group_size=10,periods=4, mean 60 → среднееlenпо файлам счетов 58/60/62/66/67/76 в пределах ±15 %,capped is False);test_cap_and_flag(mean 150,group_size=40→ 60/62/76 не большеlen(externals), 66/67 — только банки,cappedчестно отражает срез);test_sets_disjoint_from_group(ни одного члена группы и банка в 60/62/76);test_zero_mean(mean 0 → все списки пусты);test_plan_deterministic. -
Step 3: Тесты экономики (на
group_size=3,periods=4, mean 20):test_books_balanced(для каждой книги Σ сальдо всех счетов, включая000, на любую дату = 0; сальдо000после входящих остатков ≠ 0 только против остатков — проверить, что000нет оборотов послеbook_start);test_bs_identity— на конец каждого периода сальдо 90/91/99 = 0 после закрытия месяца, кроме 99 внутри года, и Σ активных = Σ пассивных сальдо (без000);test_externals_used— у каждой (компания, счёт) из плана с непустым списком в книге есть проводки по ≥ 80 % контрагентов списка за год;test_pareto_shape— по 62 у крупнейшей компании топ-10 % контрагентов дают ≥ 50 % оборота;test_signs_variety— среди сальдо по контрагентам 60/62 на конец года есть оба знака (§5.3, ~3 % противоположных);test_year_rollover(start=2025-Q3, 4 периода: сальдо 90/91/99 на 31.12.2025 = 0, 84 изменился на прибыль года, обороты 2026 начинаются с нуля по 90). -
Step 4: реализовать. Масштаб выручки — логнормальный по §5.2 (
rng_for(seed, "scale", code)), операции — по образцуeconomy.py#35 (post_month_flow,close_month), но контрагенты — из плана: сумма месяца по счёту раскладывается по контрагентам весами Парето (alpha=1.2, веса фиксированы на компанию×счёт), ~20 % контрагентов — только обороты (полная оплата в месяце), ~5 % — сальдо без оборотов (входящий остаток), ~3 % — обратный знак (аванс). Банки — кредиты 66.01/67.01 и проценты 66.02/67.02 (сверить субсчета сchart.ACCOUNTS). 01 — по субсчету 01.01 (без контрагентов), 90 — 90.01/90.02/90.03/90.07/90.08/90.09. -
Step 5: PASS всего набора.
-
Step 6: Коммит
synthgen: периоды, внешние контрагенты, экономика компаний (#38).
Task 4: Операции внутри группы и расхождения ВГО
Files:
- Create:
synthgen/intragroup.py - Test:
synthgen/tests/test_intragroup.py
Interfaces:
-
Consumes:
economy.open_books/post_month/close_monthилиbuild_ledgers(..., extra=...),Group,Period. -
Produces:
intragroup.VgoPair— frozen:a: str,b: str(коды;a— продавец/займодавец),kind: str(trade|loan),mismatch: Mismatch | None.intragroup.Mismatch— frozen:side: str(код стороны, у которой проводка есть),year: int,period_key: str,amount: Decimal,pct: Decimal(от меньшей стороны сальдо пары на конец периода),above_threshold: bool.intragroup.plan_pairs(cfg: Config, group: Group) -> list[VgoPair]— без расхождений; торговля: каждая упорядоченная пара с вероятностью 0.5 (rng_for(seed,"trade",a,b)); займы: головная → каждая дочерняя.intragroup.build(cfg, group, ps, plan) -> tuple[dict[str, Ledger], list[VgoPair]]— полный построитель книг (вызываетeconomyсextra): торговля (62.01 ↔ 60.01, объём 5–20 % выручки продавца, частичная оплата), займы (58.03 ↔ 66.03 или 67.03, проценты 76 ↔ 76 / 91), дивиденды (Дт 84 Кт 76.03 у плательщика ↔ Дт 76.03 Кт 91 у получателя во 2 квартале, оплата в том же месяце — решение 11), доли (58.01 у головной ↔ 80.09 у дочерней, миноритарий в 80.09 — входящие остатки), расхождения §5.5.intragroup.pair_balance(ledgers, pair, at: date) -> tuple[Decimal, Decimal]— сальдо уaпоbи уbпоaпо счетам пары.
-
Step 1: Тесты:
test_pairs_mirror— для пар без расхождения на конец каждого периодаbal_a == -bal_b(точно);test_mismatch_share—group_size=10, доля 0.1 → число пар с расхождением =max(1, floor(0.1 × пар))в каждом году; доля 0 → ни одной;test_mismatch_mix— при ≥ 2 расхождениях половинаpct∈ [1, 4], половина ∈ [8, 25], при нечётном лишнее — крупное;above_threshold == (pct > 5);test_mismatch_lifecycle— разница сторон =amountво всех периодах года отperiod_keyдо конца года и 0 на 31 января следующего года;test_dividends_no_balance— сальдо 76.03 по членам группы на конец каждого периода = 0, обороты 76.03 во 2 квартале есть у обеих сторон;test_shares— 58.01 у головной по дочерней = 80.09 дочерней по головной (точно), миноритарий есть только в 80.09;test_books_still_balanced(как в Task 3, на результатеbuild);test_edge_group_two(group_size=2— одна пара займа, торговля возможна, генерация без ошибок). -
Step 2: FAIL → реализация → PASS.
-
Step 3: Коммит
synthgen: операции внутри группы и расхождения ВГО (#38).
Task 5: ОСВ в оформлении 1С
Files:
- Create:
synthgen/templates/build_osv_style.py,synthgen/templates/osv_style.json,synthgen/osv.py - Test:
synthgen/tests/test_osv.py,synthgen/tests/test_osv_parser.py(маркерparser)
Interfaces:
-
Consumes: книги из
intragroup.build,Group,Period,counterparties.OSV_ACCOUNTS. -
Produces:
osv.OsvRow—level: int(0 счёт, 1 субсчёт, 2 контрагент),label: str,inn: str | None, шесть суммod, oc, td, tc, cd, cc: Decimal(сальдо нач. Дт/Кт, обороты Дт/Кт, сальдо кон. Дт/Кт).osv.rows(ledger: Ledger, account: str, period: Period, member: Member, group: Group) -> list[OsvRow]— свёртка книги (родитель = сумма детей, сальдо свёрнуто по стороне); имя контрагента: внешний —Org.name; член группы —Member.name_1c, а в ОСВ компании изgroup.no_innу члена изvariant_name— вариантное написание;inn—Noneу компаний изgroup.no_inn.osv.write(path: Path, member: Member, account: str, period: Period, rows: list[OsvRow], with_inn: bool) -> None.osv.file_name(account: str, period: Period) -> str—сч 60_9 месяцев 2025 г.xlsx,сч 62_2025 г.xlsx.osv_style.json— собираетсяbuild_osv_style.pyиз образца РБ один раз и коммитится: ширины колонок, высоты строк по типам (шапка, заголовок таблицы, строка счёта, строка контрагента), шрифты, заливкиF4ECC5/F8F2D8, рамки, объединения строк заголовка и строк данных как смещения от колонки «блок текста» и «блок сумм»; тексты шапки строк 1–10 (кроме организации и периода).
-
Step 1: Тесты оформления (на файле, записанном
osv.writeвtmp_path):test_header_texts— A1 = имя организации, A2 =Оборотно-сальдовая ведомость по счету 60 за 9 месяцев 2025 г., строки 9–10 содержатСчет/Контрагенты,Сальдо на начало периода,Обороты за период,Сальдо на конец периода,Дебет/Кредит;test_style_matches_sample— шрифт A12 Arial 8, A2 Arial 12 полужирный, заливка A9FFF4ECC5, A12FFF8F2D8, у строки контрагента заливки нет, рамки у ячеек таблицы,summaryBelow is False, outline 1/2 у субсчёта/контрагента, отступ 1/2,number_format == "#,##0.00";test_merges_follow_sample— объединения строк 9–12 совпадают с образцом (для ОСВ без ИНН — буквы те же, что в образце; с ИНН — сдвиг блока сумм на 1 колонку, ИНН в колонке E);test_zero_is_empty(нулевые суммы — пустые ячейки);test_total_row(последняя строкаИтого, суммы = сумме строк счёта);test_no_inn_file(у компании изno_innнет колонки ИНН и нет 10-значных чисел в колонке имени). -
Step 2: Тесты содержания:
test_rows_match_ledger— для одной (компания, 62, период) суммы строк контрагентов равныLedger.balance/turnover;test_parent_sums;test_group_names— член группы записанname_1c, в ОСВ компании без ИНН ровно один член группы — вариантным именем;test_osv_without_externals(mean 0 — файл 60 валиден, «Итого» есть). -
Step 3: Приёмка парсером
test_osv_parser.py— по образцуsynthetic-group/tests/test_osv_parser.py#35:TP_WTиз окружения (по умолчанию/Users/andrey.epifancev/projects/awp-ca/.wt/synthetic-group/tabular-parser, venv уже собран; если нет — тестskip), прогон по умолчанию (Config()): каждый файл →status == "done", один документ, счёт = счёт файла, у ОСВ с ИНН у контрагентов-членов группы парсер вернул ИНН, у ОСВ без ИНН — нет; суммы «Итого» документа = суммамrows. Запуск:$UVRUN python -m pytest synthgen/tests -q -m parser. -
Step 4: FAIL → реализация → PASS (обычный набор и
-m parser). -
Step 5: Коммит
synthgen: ОСВ в оформлении 1С (#38).
Task 6: ББ и ОФР по шаблону 1С
Files:
- Create:
synthgen/templates/build_forms_templates.py,synthgen/templates/bb_1c.xlsx,synthgen/templates/ofr_1c.xlsx,synthgen/forms.py,synthgen/forms_xlsx.py - Test:
synthgen/tests/test_forms.py,synthgen/tests/test_forms_xlsx.py,synthgen/tests/test_forms_parser.py(маркерparser)
Interfaces:
-
Consumes: книги,
Member,Period,rng_for. -
Produces:
forms.BALANCE_CODES,forms.INCOME_CODES— 66н изforms.py#35 (BALANCE_FULL["66n"],INCOME_FULL["66n"]) плюс2530,2900,2910в ОФР.forms.Form—kind: str(BALANCE|INCOME),member,period,columns: list[str](подписи колонок),values: dict[str, list[Decimal]](код → значения колонок, тыс. руб. целые; расходы ОФР — отрицательные).forms.balance(ledger, member, period, ps: list[Period], seed: int) -> Form;forms.income(...) -> Form. Колонки ББ: на конец периода, на 31.12 прошлого года, на 31.12 позапрошлого; ОФР: за период, за тот же период прошлого года. Правило §5.6 п. 6: колонка из книги, если дата/период внутри книги (включая 31.12 перед первым периодом = входящие остатки), иначе масштабированные значения (rng_for(seed,"prev",code,kind), множитель 0.8–1.1 к первой книжной колонке, затем пересчёт итогов и подгонка 1600=1700).- Свёртка счетов в строки —
_classify/balance_rub/income_rub/_plugизforms.py#35 (безFOOD_SUBLINES,S03_DELTA,LONG_TERM_58_03по кодам #35: долгосрочные займы — 58.03 головной). forms_xlsx.write(form: Form, path: Path) -> None;forms_xlsx.file_name(form) -> str—ББ_9 месяцев 2025 г.xlsx,ОФР_2025 г.xlsx.- Шаблоны
bb_1c.xlsx/ofr_1c.xlsx— собираютсяbuild_forms_templates.pyиз образцов Межениновской один раз и коммитятся: все формулы заменены пустыми значениями; ячейки правее последней колонки формы очищены; ФИО в подписях, реквизиты (наименование, ИНН, ОКПО, ОКВЭД, ОКОПФ/ОКФС, вид деятельности, адрес), аудитор, даты подписания — очищены (генератор пишет свои). Оформление (шрифты, рамки, объединения, высоты,[=0]"-";General, «КОДЫ») сохраняется.
-
Step 1: Тесты расчёта:
test_balance_identity— во всех колонках всех ББ прогона по умолчанию1600 == 1700, итоги разделов = суммам строк;test_balance_matches_ledger— колонка «на конец периода» ББ =balance_rubкниги / 1000 сROUND_HALF_UPпострочно (±1 только в строке подгонки);test_first_prior_column_from_book— колонка «на 31 декабря» года перед первым периодом = входящие остатки книги;test_income_matches_ledger— 2110/2120/2210/2220/2320/2330/2340/2350/2400 = обороты 90/91 за период (знаки: расходы < 0);test_2400_flow— 2400 = 2300 + 2410 + 2460 по форме. -
Step 2: Тесты файла:
test_template_sanitized— вbb_1c.xlsx/ofr_1c.xlsxнет формул (strначиная с=), нет строк7016006635,85262693,Межениновск,Томская, ФИО подписантов образца (взять из образца в тесте) и нет значений правее последней колонки формы;test_written_file_no_sample_values— то же для файлов прогона;test_codes_and_values— по колонке «Коды» найдены всеBALANCE_CODES/INCOME_CODES, значения на месте, ноль отображается форматом[=0]"-";General;test_requisites— организация =Member.name, ИНН =Member.inn, дата шапки и подписи колонок под период;test_style_kept— шрифт заголовка Times New Roman, есть объединения и рамки как в шаблоне. -
Step 3: Приёмка парсером
test_forms_parser.py— по образцу #35synthetic-group/tests/test_forms_parser.py: каждый файл прогона по умолчанию →status == "done", все коды найдены, значения =Form.values,1600 == 1700. -
Step 4: FAIL → реализация → PASS (обычный набор и
-m parser). -
Step 5: Коммит
synthgen: ББ и ОФР по шаблону 1С (#38).
Task 7: Сборка набора, README прогона, детерминированность
Files:
- Create:
synthgen/generate.py,synthgen/report.py,synthgen/README.md - Modify:
synthgen/__main__.py,README.md(корень fixtures — строка дерева проsynthgen/) - Test:
synthgen/tests/test_generate.py
Interfaces:
-
Consumes: всё выше.
-
Produces:
generate.generate(cfg: Config, out: Path, *, force: bool = False) -> Manifest— структура каталога §7 (README.md,config.yaml,directories/ecp-<prefix>.xlsx,osv/<КОД>/<период>/…,statements/<КОД>/<период>/…);outнепустой безforce→FileExistsError; сforce— очистка содержимогоout.generate.Manifest—files: list[Path],pairs: list[VgoPair],capped: bool.report.render(cfg, group, pairs, plan, manifest) -> str— README прогона §7: настройки, состав группы (код, имя, ИНН, доля головной, миноритарий), компании без ИНН и вариантное имя, пары ВГО с расхождениями (стороны, вид, период, сумма, %, «ниже/выше порога 5 %»), число контрагентов по файлам (мин/среднее/макс по счетам), «среднее ограничено словарём», еслиcapped.__main__.main— вызываетgenerate, печатает путь и сводку (файлов, пар, расхождений), код 0;FileExistsError→ сообщение и код 3.
-
Step 1: Тесты:
test_layout(прогон по умолчанию: есть все разделы §7, ОСВ = 5 компаний × 4 периода × 9 счетов, кроме пустых по правилу 1С — пустой файл всё равно пишется с «Итого»; ББ и ОФР = 5 × 4 × 2);test_determinism— два прогона с однимseedв разные каталоги побайтно равны по всем файлам; разныйseed→ хотя бы один файл различается;test_out_not_empty(безforce—FileExistsError, CLI код 3; сforce— старых файлов нет);test_edge_configs— параметризованный:group_size=2;group_size=40, periods=1, osv_counterparties_mean=10;no_inn_companies=group_size;vgo_mismatch_share=1;start=2025-Q4, periods=2—generateбез ошибок, README непустой;test_readme_lists_mismatches(каждая пара с расхождением изManifest.pairsупомянута в README с процентом);test_large_run_time(маркерslow, по умолчанию пропускается:group_size=15, periods=6, osv_counterparties_mean=150— меньше 300 с). -
Step 2: FAIL → реализация → PASS.
-
Step 3:
synthgen/README.md— назначение, настройки §3 (таблица), пример запуска, структура результата, как загрузить на стенд (импорт ЕЦП, затем документы по компаниям), ограничения (не оракул, только 66н, среднее ≤ 150, коллизия хеша префиксов), как пересобрать словарь и шаблоны. Строка в корневомREADME.md. -
Step 4: Крупный прогон и приёмка:
$UVRUN python -m synthgen --set group_size=15 --set periods=6 --set osv_counterparties_mean=150 --out /private/tmp/claude-501/synthgen-large --force— время в отчёт; затемTP_WT=… $UVRUN python -m pytest synthgen/tests -q -m parser— PASS; полный набор — PASS. -
Step 5: Коммит
synthgen: сборка набора, README, детерминированность (#38).