"""Сборка набора (спека §7): группа → периоды → книги → файлы ЕЦП, ОСВ, ББ и ОФР, config.yaml и README прогона.""" import shutil from dataclasses import dataclass, field, replace from pathlib import Path from synthgen import dictionary, ecp, forms, forms_xlsx, osv, report from synthgen.config import Config, dump_config from synthgen.counterparties import OSV_ACCOUNTS, CounterpartyPlan from synthgen.counterparties import plan as make_plan from synthgen.group import build_group from synthgen.intragroup import VgoPair, build from synthgen.periods import build_periods @dataclass class Manifest: files: list[Path] = field(default_factory=list) pairs: list[VgoPair] = field(default_factory=list) capped: bool = False def default_out(cfg: Config) -> Path: return cfg.out if cfg.out is not None else Path("out") / cfg.prefix SET_MARKER = "# Синтетический набор" class NotASetError(FileExistsError): """`--force` отказывается чистить каталог, не похожий на набор synthgen.""" def _looks_like_set(out: Path) -> bool: readme, config = out / "README.md", out / "config.yaml" if not (readme.is_file() and config.is_file()): return False with readme.open(encoding="utf-8", errors="replace") as fh: return fh.readline().startswith(SET_MARKER) def _prepare(out: Path, force: bool) -> None: if out.exists() and not out.is_dir(): raise FileExistsError(f"каталог результата не каталог, а файл: {out}") if out.exists() and any(out.iterdir()): if not force: raise FileExistsError(f"каталог результата не пуст: {out} (--force перезапишет прежний набор)") if (out / ".git").exists(): raise NotASetError(f"{out} содержит .git: чистить такой каталог нельзя") if not _looks_like_set(out): raise NotASetError(f"{out} не похож на набор synthgen (нет config.yaml и README.md с заголовком набора): не чищу") for item in out.iterdir(): shutil.rmtree(item) if item.is_dir() and not item.is_symlink() else item.unlink() out.mkdir(parents=True, exist_ok=True) def generate(cfg: Config, out: Path, *, force: bool = False) -> Manifest: """Строит набор в каталоге `out`; непустой `out` без `force` — FileExistsError.""" _prepare(out, force) group = build_group(cfg, dictionary.load()) periods = build_periods(cfg.start, cfg.periods) plan: CounterpartyPlan = make_plan(cfg, group) books, pairs = build(cfg, group, periods, plan) manifest = Manifest(pairs=pairs, capped=plan.capped) def keep(path: Path) -> Path: manifest.files.append(path) return path ecp.write_ecp(group, keep(out / "directories" / f"ecp-{cfg.prefix}.xlsx")) for member in group.members: ledger = books[member.code] with_inn = member.code not in group.no_inn for period in periods: for account in OSV_ACCOUNTS: data = osv.rows(ledger, account, period, member, group) path = out / "osv" / member.code / period.key / osv.file_name(account, period) osv.write(path, member, account, period, data, with_inn=with_inn) keep(path) for form in ( forms.balance(ledger, member, period, periods, cfg.seed), forms.income(ledger, member, period, periods, cfg.seed), ): path = out / "statements" / member.code / period.key / forms_xlsx.file_name(form) forms_xlsx.write(form, path) keep(path) (out / "config.yaml").write_text(dump_config(replace(cfg, out=None)), encoding="utf-8") keep(out / "config.yaml") readme = out / "README.md" manifest.files.append(readme) readme.write_text(report.render(cfg, group, pairs, plan, manifest), encoding="utf-8") return manifest