"""Приёмка ОСВ парсером: CLI tabular-parser из зеркального worktree (`TP_WT`), маркер `parser`. Запуск: `$UVRUN python -m pytest synthgen/tests -q -m parser`. Без окружения тесты пропускаются. """ import json import os import subprocess from decimal import Decimal from pathlib import Path import pytest from synthgen import osv pytestmark = pytest.mark.parser TP_WT = Path(os.environ.get("TP_WT") or "/Users/andrey.epifancev/projects/awp-ca/.wt/synthetic-group/tabular-parser") PY = TP_WT / "api" / ".venv" / "bin" / "python" ACCOUNTS = ("60", "62", "76") def _run(path: Path) -> dict: env = {**os.environ, "ENV_FILE": str(TP_WT / "api" / ".env.test"), "PYTHONPATH": str(TP_WT / "api")} proc = subprocess.run( [str(PY), "-m", "src.parser.cli", str(path)], cwd=TP_WT / "api", env=env, capture_output=True, text=True, check=False ) return json.loads(proc.stdout) @pytest.fixture(scope="module") def parsed(osv_world, tmp_path_factory): if not PY.exists(): pytest.skip(f"нет окружения парсера: {PY}") cfg, group, periods, books = osv_world root = tmp_path_factory.mktemp("osv") out = [] for member in group.members: with_inn = member.code not in group.no_inn for account in ACCOUNTS: for period in (periods[0], periods[-1]): data = osv.rows(books[member.code], account, period, member, group) path = root / f"{member.code}_{period.key}_{osv.file_name(account, period)}" osv.write(path, member, account, period, data, with_inn=with_inn) out.append((member, account, period, data, with_inn, _run(path))) return out def test_parser_status_and_account(parsed): bad = [] for member, account, period, _, _, res in parsed: docs = res.get("documents", []) if res.get("status") != "done" or len(docs) != 1 or docs[0]["header"]["account_code_raw"] != account: bad.append((member.code, account, period.key, res.get("status"), res.get("error"))) assert not bad, bad[:10] def test_parser_inn(parsed, osv_world): _, group, _, _ = osv_world group_names = {m.name_1c: m.inn for m in group.members} 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"] if with_inn: for r in got: if r["counterparty_name"] in group_names and r["counterparty_inn"] != group_names[r["counterparty_name"]]: bad.append((member.code, account, r["counterparty_name"], r["counterparty_inn"])) elif any(r["counterparty_inn"] for r in got): bad.append((member.code, account, "ИНН у ОСВ без ИНН")) assert not bad, bad[:10] def test_parser_totals(parsed): """Сумма корневой строки счёта в документе = «Итого» листа = суммам `rows`; проверки парсера чистые.""" keys = ("opening_debit", "opening_credit", "turnover_debit", "turnover_credit", "closing_debit", "closing_credit") cent = Decimal("0.01") bad = [] for member, account, period, data, _, res in parsed: doc = res["documents"][0] checks = doc["checks"] if not checks["total_row_matches_root"] or checks["parent_sum_violations"] or checks["balance_identity_violations"]: bad.append((member.code, account, period.key, checks)) root = next(r for r in doc["rows"] if r["group_type"] == "ACCOUNT" and r["data_type"] == "BU") got = tuple(Decimal(str(root[k] or 0)).quantize(cent) for k in keys) if got != tuple(v.quantize(cent) for v in osv.total_of(data)): bad.append((member.code, account, period.key, got)) assert not bad, bad[:10]