diff --git a/synthgen/osv.py b/synthgen/osv.py index 008f01b..cc180f2 100644 --- a/synthgen/osv.py +++ b/synthgen/osv.py @@ -47,6 +47,7 @@ class OsvRow: cd: Decimal # сальдо на конец, Дт cc: Decimal # сальдо на конец, Кт is_cp: bool = False # строка контрагента + cp: str | None = None # код аналитики контрагента в книге (для сверки с Ledger) @property def six(self) -> Six: @@ -196,7 +197,7 @@ def rows(ledger: Ledger, account: str, period: Period, member: Member, group: Gr for name, inn, cp in named: six = by_cp[cp] total = _add(total, six) - out.append(OsvRow(level, name, inn, *six, is_cp=True)) + out.append(OsvRow(level, name, inn, *six, is_cp=True, cp=cp)) return out, total def build(code: str, level: int) -> tuple[list[OsvRow], Six]: diff --git a/synthgen/tests/test_osv.py b/synthgen/tests/test_osv.py index 91f43ad..222fbf9 100644 --- a/synthgen/tests/test_osv.py +++ b/synthgen/tests/test_osv.py @@ -103,19 +103,46 @@ def test_no_inn_file(tmp_path, osv_world): def test_rows_match_ledger(osv_world): - member, period, data, group, books = _pick(osv_world, "62", inn=True) - ledger = books[member.code] - bal0 = ledger.balance(period.start - timedelta(days=1), "62") - bal1 = ledger.balance(period.end, "62") - turn = ledger.turnover(period.start, period.end, "62") - cps = [r for r in data if r.is_cp] - assert cps - for key, (td, tc) in turn.items(): - assert key[1] is not None - assert sum(r.td for r in cps) == sum(t[0] for t in turn.values()) - assert sum(r.tc for r in cps) == sum(t[1] for t in turn.values()) - assert sum(r.cd - r.cc for r in cps) == sum(bal1.values()) - assert sum(r.od - r.oc for r in cps) == sum(bal0.values()) + """Каждая строка контрагента равна Ledger.balance/turnover по ключу (субсчёт, контрагент).""" + cfg, group, periods, books = osv_world + checked = 0 + for member in group.members: + ledger = books[member.code] + for period in periods: + data = osv.rows(ledger, "62", period, member, group) + opening = ledger.balance(period.start - timedelta(days=1), "62") + closing = ledger.balance(period.end, "62") + turn = ledger.turnover(period.start, period.end, "62") + + def fold(src, pick): + out = {} + for (code, an), v in src.items(): + key = (code, an.cp if an else None) + out[key] = out.get(key, ZERO) + pick(v) + return out + + want = { + "o": fold(opening, lambda v: v), "c": fold(closing, lambda v: v), + "td": fold(turn, lambda v: v[0]), "tc": fold(turn, lambda v: v[1]), + } + code = None + seen = set() + for r in data: + if not r.is_cp: + code = r.label + continue + key = (code, r.cp) + seen.add(key) + assert r.od - r.oc == want["o"].get(key, ZERO), (member.code, period.key, key) + assert r.cd - r.cc == want["c"].get(key, ZERO), (member.code, period.key, key) + assert r.td == want["td"].get(key, ZERO) and r.tc == want["tc"].get(key, ZERO), (member.code, period.key, key) + checked += 1 + # обратное: всё ненулевое из книги попало в строки + for kind, src in want.items(): + for key, v in src.items(): + if v != 0: + assert key in seen, (member.code, period.key, kind, key) + assert checked > 20 def test_parent_sums(osv_world): @@ -138,19 +165,30 @@ def test_parent_sums(osv_world): def test_group_names(osv_world): + """В ОСВ без ИНН ровно один член группы записан вариантным именем, остальные — `name_1c`; с ИНН — все `name_1c`.""" cfg, group, periods, books = osv_world + by_code = {m.code: m for m in group.members} + variants_seen = 0 for member in group.members: - data = osv.rows(books[member.code], "62", periods[-1], member, group) - names = [r.label for r in data if r.is_cp] others = [m for m in group.members if m.code != member.code] - present = [m for m in others if m.name_1c in names or osv.variant_spelling(m) in names] - if member.code in group.no_inn: - variant = next(m for m in group.members if m.code == group.variant_name[member.code]) - assert not any(m.name_1c in names for m in others if m is variant) - assert all(r.inn is None for r in data) - else: - assert all(m.name_1c in names for m in present) - assert all(r.inn for r in data if r.is_cp) + for account in ("60", "62", "76"): + for period in periods: + data = osv.rows(books[member.code], account, period, member, group) + cps = {r.cp: r for r in data if r.is_cp} + for m in others: + if m.code not in cps: + continue + r = cps[m.code] + if member.code in group.no_inn and group.variant_name[member.code] == m.code: + assert r.label == osv.variant_spelling(m) != m.name_1c + variants_seen += 1 + else: + assert r.label == m.name_1c, (member.code, account, period.key, m.code) + assert r.inn == (None if member.code in group.no_inn else m.inn) + if member.code in group.no_inn: + variant = by_code[group.variant_name[member.code]] + assert sum(1 for r in data if r.is_cp and r.label == variant.name_1c) == 0 + assert variants_seen > 0, "вариантное написание нигде не встретилось" def test_osv_without_externals(tmp_path): diff --git a/synthgen/tests/test_osv_parser.py b/synthgen/tests/test_osv_parser.py index fe7c270..0487968 100644 --- a/synthgen/tests/test_osv_parser.py +++ b/synthgen/tests/test_osv_parser.py @@ -25,6 +25,7 @@ def _run(path: Path) -> dict: proc = subprocess.run( [str(PY), "-m", "src.parser.cli", str(path)], cwd=TP_WT / "api", env=env, capture_output=True, text=True, check=False ) + assert proc.stdout.strip(), proc.stderr return json.loads(proc.stdout) @@ -61,6 +62,7 @@ def test_parser_inn(parsed, osv_world): bad = [] for member, account, period, data, with_inn, res in parsed: got = [r for r in res["documents"][0]["rows"] if r["group_type"] == "COUNTERPARTY" and r["data_type"] == "BU"] + assert got, (member.code, account, period.key) if with_inn: for r in got: if r["counterparty_name"] in group_names and r["counterparty_inn"] != group_names[r["counterparty_name"]]: