from datetime import timedelta from decimal import Decimal import openpyxl import pytest from synthgen import osv from synthgen.periods import build_periods ZERO = Decimal(0) SAMPLE_MERGES = { "A9:D9", "A10:D11", "E9:I9", "E10:G11", "H10:I11", "J9:N9", "J10:K11", "L10:N11", "O9:Q9", "O10:P11", "Q10:Q11", } def _pick(osv_world, account="62", inn=True): cfg, group, periods, books = osv_world member = next(m for m in group.members if (m.code not in group.no_inn) == inn) period = periods[-1] data = osv.rows(books[member.code], account, period, member, group) return member, period, data, group, books def _write(tmp_path, osv_world, account="62", inn=True): member, period, data, group, books = _pick(osv_world, account, inn) path = tmp_path / osv.file_name(account, period) osv.write(path, member, account, period, data, with_inn=inn) return openpyxl.load_workbook(path).active, member, period, data def test_file_name(): ps = build_periods("2025-Q1", 4) assert osv.file_name("60", ps[2]) == "сч 60_9 месяцев 2025 г.xlsx" assert osv.file_name("62", ps[3]) == "сч 62_2025 г.xlsx" def test_header_texts(tmp_path, osv_world): ws, member, period, _ = _write(tmp_path, osv_world, "60") assert ws["A1"].value == member.name assert ws["A2"].value == f"Оборотно-сальдовая ведомость по счету 60 за {period.label_1c}" text9 = " ".join(str(c.value) for c in ws[9] if c.value) text10 = " ".join(str(c.value) for c in ws[10] if c.value) assert "Счет" in text9 and "Сальдо на начало периода" in text9 assert "Обороты за период" in text9 and "Сальдо на конец периода" in text9 assert "Контрагенты" in text10 and "Дебет" in text10 + " ".join(str(c.value) for c in ws[11] if c.value) assert "Кредит" in text10 + " ".join(str(c.value) for c in ws[11] if c.value) def test_style_matches_sample(tmp_path, osv_world): ws, _, _, data = _write(tmp_path, osv_world, "62", inn=False) assert ws["A12"].font.name == "Arial" and ws["A12"].font.sz == 8 assert ws["A2"].font.name == "Arial" and ws["A2"].font.sz == 12 and ws["A2"].font.b assert ws["A9"].fill.fgColor.rgb == "FFF4ECC5" assert ws["A12"].fill.fgColor.rgb == "FFF8F2D8" cp_row = next(i for i, r in enumerate(data, start=12) if r.is_cp) sub_row = next(i for i, r in enumerate(data, start=12) if r.level == 1) assert ws.cell(cp_row, 1).fill.fill_type is None assert ws["A9"].border.left.style and ws.cell(cp_row, 5).border.bottom.style assert ws.sheet_properties.outlinePr.summaryBelow is False assert ws.row_dimensions[sub_row].outlineLevel == 1 and ws.row_dimensions[cp_row].outlineLevel == 2 assert ws.cell(sub_row, 1).alignment.indent == 1 and ws.cell(cp_row, 1).alignment.indent == 2 number = next(c for c in ws[cp_row] if isinstance(c.value, float)) assert number.number_format == "#,##0.00" def test_merges_follow_sample(tmp_path, osv_world): ws, *_ = _write(tmp_path, osv_world, "62", inn=False) got = {str(m) for m in ws.merged_cells.ranges if m.min_row in (9, 10, 11)} assert SAMPLE_MERGES <= got (tmp_path / "x").mkdir() ws2, *_ = _write(tmp_path / "x", osv_world, "62", inn=True) got2 = {str(m) for m in ws2.merged_cells.ranges if m.min_row in (9, 10, 11)} assert {"A9:D9", "A10:D11", "E10:E11", "F9:J9", "F10:H11", "I10:J11", "K10:L11", "M10:O11", "P10:Q11", "R10:R11"} <= got2 assert ws2["E10"].value == "ИНН" def test_zero_is_empty(tmp_path, osv_world): ws, _, _, data = _write(tmp_path, osv_world, "62", inn=False) for i, row in enumerate(data, start=12): spans = [(5, 7), (8, 9), (10, 11), (12, 14), (15, 16), (17, 17)] for (a, _), value in zip(spans, row.six, strict=True): cell = ws.cell(i, a).value assert (cell is None) if value == 0 else (cell == float(value)) def test_total_row(tmp_path, osv_world): ws, _, _, data = _write(tmp_path, osv_world, "62", inn=False) last = 11 + len(data) + 1 assert ws.cell(last, 1).value == "Итого" totals = osv.total_of(data) account_row = next(r for r in data if r.level == 0) assert totals == account_row.six for (a, _), value in zip([(5, 7), (8, 9), (10, 11), (12, 14), (15, 16), (17, 17)], totals, strict=True): assert (ws.cell(last, a).value or 0) == pytest.approx(float(value)) def test_no_inn_file(tmp_path, osv_world): ws, member, _, data = _write(tmp_path, osv_world, "62", inn=False) assert ws.max_column <= 17 assert all(not (isinstance(c.value, str) and c.value.isdigit() and len(c.value) == 10) for c in ws["A"]) assert all(r.inn is None for r in data) assert all(ws.cell(i, 5).value is None or isinstance(ws.cell(i, 5).value, float) for i in range(12, 12 + len(data))) 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()) def test_parent_sums(osv_world): _, _, data, _, _ = _pick(osv_world, "76") assert data[0].level == 0 subs = [r for r in data if r.level == 1] for col in range(6): assert data[0].six[col] == sum(r.six[col] for r in subs) # у субсчёта контрагенты суммируются в его строку for i, r in enumerate(data): if r.level == 1: kids = [] for k in data[i + 1:]: if k.level <= 1: break if k.is_cp: kids.append(k) if kids: assert r.six == tuple(sum(k.six[c] for k in kids) for c in range(6)) def test_group_names(osv_world): cfg, group, periods, books = osv_world 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) def test_osv_without_externals(tmp_path): from synthgen import dictionary from synthgen.config import Config from synthgen.counterparties import plan as make_plan from synthgen.group import build_group from synthgen.intragroup import build cfg = Config(group_size=3, periods=2, osv_counterparties_mean=0) group = build_group(cfg, dictionary.load()) ps = build_periods(cfg.start, cfg.periods) books, _ = build(cfg, group, ps, make_plan(cfg, group)) member = group.members[0] data = osv.rows(books[member.code], "60", ps[-1], member, group) path = tmp_path / "x.xlsx" osv.write(path, member, "60", ps[-1], data, with_inn=member.code not in group.no_inn) ws = openpyxl.load_workbook(path).active assert any(c.value == "Итого" for c in ws["A"])