from datetime import date from decimal import Decimal import pytest from synthgen import dictionary from synthgen.config import Config from synthgen.counterparties import plan as make_plan from synthgen.economy import build_ledgers, last_day, months from synthgen.group import build_group from synthgen.periods import book_start, build_periods D = Decimal def _build(**kw): cfg = Config(group_size=kw.pop("group_size", 3), periods=kw.pop("periods", 4), osv_counterparties_mean=kw.pop("mean", 20), **kw) g = build_group(cfg, dictionary.load()) ps = build_periods(cfg.start, cfg.periods) pl = make_plan(cfg, g) return cfg, g, ps, pl, build_ledgers(cfg, g, ps, pl) @pytest.fixture(scope="module") def built(): return _build() def test_books_balanced(built): cfg, g, ps, pl, books = built start = book_start(ps) for lg in books.values(): for day in (start, date(2025, 3, 31), date(2025, 12, 31)): assert sum(lg.balance(day).values()) == 0 assert lg.balance(day, "000") == {} assert not [p for p in lg.postings if "000" in (p.dt, p.ct) and p.day > start] def test_bs_identity(built): cfg, g, ps, pl, books = built for lg in books.values(): for p in ps: day = p.end for acct in ("90", "91"): assert lg.balance(day, acct) == {} if p.quarter == 4: assert lg.balance(day, "99") == {} bal = lg.balance(day) assert sum(v for (a, _), v in bal.items() if a != "000") == 0 # активы не уходят в минус: денежные средства на конец каждого месяца for y, m in months(ps): assert lg.balance(last_day(y, m), "51").get(("51", None), D(0)) >= 0 def test_externals_used(built): cfg, g, ps, pl, books = built for (code, acct), lst in pl.by_file.items(): if not lst or acct not in ("58", "60", "62", "66", "67", "76"): continue seen = set() for p in books[code].postings: for an in (p.dt_an, p.ct_an): if an is not None and an.cp: seen.add(an.cp) used = sum(1 for o in lst if o.inn in seen) assert used >= 0.8 * len(lst), (code, acct, used, len(lst)) def test_pareto_shape(built): cfg, g, ps, pl, books = built tot = {code: sum(c for (a, _), (_, c) in lg.turnover(date(2025, 1, 1), date(2025, 12, 31), "62").items()) for code, lg in books.items()} biggest = max(tot, key=tot.get) by_cp = {} for (a, an), (dt, ct) in books[biggest].turnover(date(2025, 1, 1), date(2025, 12, 31), "62").items(): if an is not None: by_cp[an.cp] = by_cp.get(an.cp, D(0)) + dt + ct vals = sorted(by_cp.values(), reverse=True) top = max(1, len(vals) // 10) assert sum(vals[:top]) >= D("0.5") * sum(vals) def test_signs_variety(built): cfg, g, ps, pl, books = built for acct in ("60", "62"): signs = set() for lg in books.values(): for v in lg.balance(date(2025, 12, 31), acct).values(): signs.add(v > 0) assert signs == {True, False}, acct def test_year_rollover(): cfg, g, ps, pl, books = _build(start="2025-Q3") dec = date(2025, 12, 31) for lg in books.values(): assert lg.balance(dec, "90") == {} and lg.balance(dec, "91") == {} and lg.balance(dec, "99") == {} assert lg.balance(dec, "84") != lg.balance(date(2024, 12, 31), "84") t = lg.turnover(date(2026, 1, 1), date(2026, 1, 1), "90") assert t == {} # 2026 начинается с нуля; обороты 90 в январе — не 1 января jan = lg.turnover(date(2026, 1, 1), date(2026, 1, 31), "90") assert any(dt or ct for dt, ct in jan.values()) assert max(p.day for lg in books.values() for p in lg.postings) == date(2026, 6, 30) def test_extra_hook_called_before_close(): cfg = Config(group_size=2, periods=1, osv_counterparties_mean=5) g = build_group(cfg, dictionary.load()) ps = build_periods(cfg.start, 1) seen = [] def hook(ledgers, year, month): seen.append((year, month, len(ledgers["C01"].postings))) build_ledgers(cfg, g, ps, make_plan(cfg, g), extra=hook) assert [(y, m) for y, m, _ in seen] == [(2025, 1), (2025, 2), (2025, 3)] def test_deterministic_and_zero_mean(): a = _build(mean=0)[4] b = _build(mean=0)[4] assert [p for p in a["C01"].postings] == [p for p in b["C01"].postings] assert a["C01"].postings def test_90_closes_via_909(): cfg, g, ps, pl, books = _build() for lg in books.values(): day = date(2025, 3, 31) assert lg.balance(day, "90") == {} t = lg.turnover(date(2025, 1, 1), day, "90.09") assert t and all(dt and ct for dt, ct in t.values()) for p in lg.postings: if p.ct == "90.09" or p.dt == "90.09": assert p.memo.startswith("закрытие на ") assert not [p for p in lg.postings if p.memo == "закрытие на 99" and p.dt == "99" and p.ct.startswith("90.0") and p.ct != "90.09"] def test_build_ledgers_deterministic_with_counterparties(): a = _build(mean=30, seed=3)[4] b = _build(mean=30, seed=3)[4] assert a.keys() == b.keys() for code in a: assert a[code].postings == b[code].postings