synthgen: тесты ОСВ сверяют контрагентов с книгой и вариантные имена (#38)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-13 00:22:40 +04:00
parent 6fb42be8ff
commit 7a4a44a6b3
3 changed files with 65 additions and 24 deletions

View File

@ -47,6 +47,7 @@ class OsvRow:
cd: Decimal # сальдо на конец, Дт cd: Decimal # сальдо на конец, Дт
cc: Decimal # сальдо на конец, Кт cc: Decimal # сальдо на конец, Кт
is_cp: bool = False # строка контрагента is_cp: bool = False # строка контрагента
cp: str | None = None # код аналитики контрагента в книге (для сверки с Ledger)
@property @property
def six(self) -> Six: 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: for name, inn, cp in named:
six = by_cp[cp] six = by_cp[cp]
total = _add(total, six) 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 return out, total
def build(code: str, level: int) -> tuple[list[OsvRow], Six]: def build(code: str, level: int) -> tuple[list[OsvRow], Six]:

View File

@ -103,19 +103,46 @@ def test_no_inn_file(tmp_path, osv_world):
def test_rows_match_ledger(osv_world): def test_rows_match_ledger(osv_world):
member, period, data, group, books = _pick(osv_world, "62", inn=True) """Каждая строка контрагента равна Ledger.balance/turnover по ключу (субсчёт, контрагент)."""
ledger = books[member.code] cfg, group, periods, books = osv_world
bal0 = ledger.balance(period.start - timedelta(days=1), "62") checked = 0
bal1 = ledger.balance(period.end, "62") for member in group.members:
turn = ledger.turnover(period.start, period.end, "62") ledger = books[member.code]
cps = [r for r in data if r.is_cp] for period in periods:
assert cps data = osv.rows(ledger, "62", period, member, group)
for key, (td, tc) in turn.items(): opening = ledger.balance(period.start - timedelta(days=1), "62")
assert key[1] is not None closing = ledger.balance(period.end, "62")
assert sum(r.td for r in cps) == sum(t[0] for t in turn.values()) turn = ledger.turnover(period.start, period.end, "62")
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()) def fold(src, pick):
assert sum(r.od - r.oc for r in cps) == sum(bal0.values()) 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): def test_parent_sums(osv_world):
@ -138,19 +165,30 @@ def test_parent_sums(osv_world):
def test_group_names(osv_world): def test_group_names(osv_world):
"""В ОСВ без ИНН ровно один член группы записан вариантным именем, остальные — `name_1c`; с ИНН — все `name_1c`."""
cfg, group, periods, books = osv_world cfg, group, periods, books = osv_world
by_code = {m.code: m for m in group.members}
variants_seen = 0
for member in group.members: 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] 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] for account in ("60", "62", "76"):
if member.code in group.no_inn: for period in periods:
variant = next(m for m in group.members if m.code == group.variant_name[member.code]) data = osv.rows(books[member.code], account, period, member, group)
assert not any(m.name_1c in names for m in others if m is variant) cps = {r.cp: r for r in data if r.is_cp}
assert all(r.inn is None for r in data) for m in others:
else: if m.code not in cps:
assert all(m.name_1c in names for m in present) continue
assert all(r.inn for r in data if r.is_cp) 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): def test_osv_without_externals(tmp_path):

View File

@ -25,6 +25,7 @@ def _run(path: Path) -> dict:
proc = subprocess.run( proc = subprocess.run(
[str(PY), "-m", "src.parser.cli", str(path)], cwd=TP_WT / "api", env=env, capture_output=True, text=True, check=False [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) return json.loads(proc.stdout)
@ -61,6 +62,7 @@ def test_parser_inn(parsed, osv_world):
bad = [] bad = []
for member, account, period, data, with_inn, res in parsed: 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"] 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: if with_inn:
for r in got: for r in got:
if r["counterparty_name"] in group_names and r["counterparty_inn"] != group_names[r["counterparty_name"]]: if r["counterparty_name"] in group_names and r["counterparty_inn"] != group_names[r["counterparty_name"]]: