173 lines
7.7 KiB
Python
173 lines
7.7 KiB
Python
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"])
|