2026-11-13 00:22:40 +04:00

211 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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):
"""Каждая строка контрагента равна 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):
_, _, 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):
"""В ОСВ без ИНН ровно один член группы записан вариантным именем, остальные — `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:
others = [m for m in group.members if m.code != member.code]
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):
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"])