arm-core/api/tests/test_intercompany_matrix_golden.py

227 lines
11 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.

"""Точные пары «Русского бисквита» на 30.09.2024 из ОСВ и ручного эталона.
Решение владельца 08.10.2026: общий итог элиминации с книгой не сравниваем —
аналитик снимает полную сумму каждой стороны, система использует минимум пары.
С листом «01.10.2024» сверяются только точные встречные строки; расхождения по
остальным строкам ожидаемы. Для правила № 6: 60.01 АО РБ ↔ 62.01
Счастливочной — производство молочной булочки (строка 21), не аренда (строка 17).
По займу 42 млн есть 66.03 АО РБ, но в фикстурах нет встречной ОСВ 58.03
Счастливочной; её присутствие проверяется отдельно на модельной строке.
"""
import json
import uuid
from decimal import ROUND_HALF_UP, Decimal
from pathlib import Path
from types import SimpleNamespace
import pytest
from openpyxl import load_workbook
from src.schemas.consolidations import IntercompanyMatrixCompanyOut
from src.services import intercompany_matrix as matrix
from src.services.intercompany_matrix import MatrixSelection, calculate_matrix
pytestmark = pytest.mark.fixtures
GOLDEN = Path("golden/expected_osv_parse")
RB = uuid.UUID("11111111-1111-1111-1111-111111111111")
BAKERY = uuid.UUID("22222222-2222-2222-2222-222222222222")
def _source(fixtures_dir: Path, folder: str, account: str, number: int) -> dict:
matches = [
row
for path in sorted((fixtures_dir / GOLDEN / folder / "2024-Q3").glob("*.json"))
for document in json.loads(path.read_text("utf-8"))["documents"]
if document["document_type"] == "OSV" and document["header"]["account_code_raw"] == account
for row in document["rows"]
if row["row_number"] == number and row["group_type"] == "COUNTERPARTY" and row["data_type"] == "BU"
]
assert len(matches) == 1, (folder, account, number)
return matches[0]
@pytest.mark.parametrize(
("debit_root", "debit_row", "credit_root", "credit_row", "amount", "golden_cells"),
[
("62", 21, "60", 409, "106311116.46", ("F51",)),
# F50 содержит 42 млн основного долга + 350 тыс. процентов; J29 — основной долг.
("76", 20, "66", 28, "350000.00", ("F50", "J29")),
],
)
def test_exact_pairs_against_russian_biscuit_golden( # noqa: PLR0913 - описание пары и ячеек эталона
fixtures_dir: Path,
*,
debit_root: str,
debit_row: int,
credit_root: str,
credit_row: int,
amount: str,
golden_cells: tuple[str, ...],
) -> None:
debit = _source(fixtures_dir, "schastl", debit_root, debit_row)
credit = _source(fixtures_dir, "rb", credit_root, credit_row)
if amount == "106311116.46":
assert (debit["account_code_raw"], credit["account_code_raw"]) == ("62.01", "60.01") # правило № 6
assert debit["counterparty_inn"] == "3528052718"
assert "СЧАСТЛИВОЧНАЯ" in credit["counterparty_name"]
debit_account, credit_account = uuid.uuid4(), uuid.uuid4()
rows = [
SimpleNamespace(
legal_entity_id=owner,
counterparty_id=counterparty,
account_id=account_id,
closing_debit=Decimal(source["closing_debit"] or "0"),
closing_credit=Decimal(source["closing_credit"] or "0"),
entry_ids=(uuid.uuid4(),),
)
for owner, counterparty, account_id, source in (
(BAKERY, RB, debit_account, debit),
(RB, BAKERY, credit_account, credit),
)
]
result = calculate_matrix(
rows,
[
IntercompanyMatrixCompanyOut(id=BAKERY, name="Пекарня Счастливочная"),
IntercompanyMatrixCompanyOut(id=RB, name="Русский бисквит"),
],
MatrixSelection(frozenset({debit_account}), frozenset({credit_account}), "closing"),
)
cell = result.cells[0]
assert cell.amount_a == cell.amount_b == Decimal(amount)
assert cell.variance_amount == cell.variance_percent == Decimal(0)
assert cell.one_sided is False and cell.exceeds_threshold is False
with (fixtures_dir / "golden/Консолидация_Группа РБ.xlsx").open("rb") as source:
workbook = load_workbook(source, read_only=True, data_only=True)
try:
sheet = workbook["01.10.2024"]
expected = Decimal(str(sheet[golden_cells[0]].value))
for address in golden_cells[1:]:
expected -= Decimal(str(sheet[address].value))
finally:
workbook.close()
# В книге тысячи рублей с округлением до целого; входные ОСВ содержат рубли и копейки.
assert (cell.amount_a / 1000).quantize(Decimal("1"), rounding=ROUND_HALF_UP) == expected
def test_loan_66_03_against_58_03_when_counterpart_exists(fixtures_dir: Path) -> None:
"""42 млн на 66.03 РБ: без ОСВ 58.03 пара односторонняя, со встречной строкой — точная."""
credit = _source(fixtures_dir, "rb", "66", 25)
assert credit["account_code_raw"] == "66.03"
amount = Decimal("42000000.00")
assert Decimal(credit["closing_credit"]) == amount
debit_account, credit_account = uuid.uuid4(), uuid.uuid4()
credit_row = SimpleNamespace(
legal_entity_id=RB,
counterparty_id=BAKERY,
account_id=credit_account,
account_code="66.03",
closing_debit=Decimal(0),
closing_credit=amount,
entry_ids=(uuid.uuid4(),),
)
companies = [
IntercompanyMatrixCompanyOut(id=BAKERY, name="Пекарня Счастливочная"),
IntercompanyMatrixCompanyOut(id=RB, name="Русский бисквит"),
]
selection = MatrixSelection(frozenset({debit_account}), frozenset({credit_account}), "closing") # правило № 4
one_sided = calculate_matrix([credit_row], companies, selection).cells[0]
assert one_sided.amount_a == 0
assert one_sided.amount_b == amount
assert one_sided.one_sided is True
context = matrix.PairContext(SimpleNamespace(id=uuid.uuid4(), side_1="D", elimination_mode="min"), BAKERY, RB)
unmatched = matrix._pack_preset_rows([credit_row], selection, context)
assert unmatched.matched == []
assert [item.reason for item in unmatched.unpaired] == ["NO_MATCH"]
assert unmatched.totals.eliminated_amount == 0
# В наборе нет ОСВ 58.03 Счастливочной; модельная строка проверяет встречную сторону правила № 4.
debit_row = SimpleNamespace(
legal_entity_id=BAKERY,
counterparty_id=RB,
account_id=debit_account,
account_code="58.03",
closing_debit=amount,
closing_credit=Decimal(0),
entry_ids=(uuid.uuid4(),),
)
paired = calculate_matrix([debit_row, credit_row], companies, selection).cells[0]
assert paired.amount_a == paired.amount_b == amount
assert paired.variance_amount == 0
assert paired.one_sided is False
packed = matrix._pack_preset_rows([debit_row, credit_row], selection, context)
assert len(packed.matched) == 1
assert packed.matched[0].amount_a == packed.matched[0].amount_b == amount
assert packed.totals.eliminated_amount == amount
def test_rule_6_bakery_and_russian_biscuit_packs_only_exact_production_row(fixtures_dir: Path) -> None:
"""Четыре реальные дебетовые строки дают итог тройки; упаковка берёт лишь точную 62.01 ↔ 60.01."""
source_rows = [
_source(fixtures_dir, "schastl", root, number)
for root, number in (("62", 21), ("62", 17), ("76", 20), ("76", 29))
]
credit_source = _source(fixtures_dir, "rb", "60", 409)
assert [(row["account_code_raw"], Decimal(row["closing_debit"])) for row in source_rows] == [
("62.01", Decimal("106311116.46")),
("62.01", Decimal("1401330.00")),
("76.06", Decimal("350000.00")),
("76.АВ", Decimal("2366666.69")),
]
assert (credit_source["account_code_raw"], Decimal(credit_source["closing_credit"])) == (
"60.01",
Decimal("106311116.46"),
)
account_ids = {code: uuid.uuid4() for code in ("62.01", "76.06", "76.АВ", "60.01")}
source_ids = [uuid.uuid4() for _ in source_rows]
credit_id = uuid.uuid4()
rows = [
SimpleNamespace(
legal_entity_id=BAKERY,
counterparty_id=RB,
account_id=account_ids[source["account_code_raw"]],
account_code=source["account_code_raw"],
closing_debit=Decimal(source["closing_debit"]),
closing_credit=Decimal(source["closing_credit"] or "0"),
entry_ids=(entry_id,),
)
for source, entry_id in zip(source_rows, source_ids, strict=True)
]
rows.append(
SimpleNamespace(
legal_entity_id=RB,
counterparty_id=BAKERY,
account_id=account_ids["60.01"],
account_code="60.01",
closing_debit=Decimal(credit_source["closing_debit"] or "0"),
closing_credit=Decimal(credit_source["closing_credit"]),
entry_ids=(credit_id,),
)
)
selection = MatrixSelection(
frozenset(account_ids[code] for code in ("62.01", "76.06", "76.АВ")),
frozenset({account_ids["60.01"]}),
"closing",
)
packed = matrix._pack_preset_rows(
rows,
selection,
matrix.PairContext(SimpleNamespace(id=uuid.uuid4(), side_1="D", elimination_mode="min"), BAKERY, RB),
)
assert (packed.totals.amount_a, packed.totals.amount_b) == (
Decimal("110429113.15"),
Decimal("106311116.46"),
)
assert packed.totals.eliminated_amount == Decimal("106311116.46")
assert packed.totals.deviation_percent is not None
assert packed.totals.deviation_percent.quantize(Decimal("0.01")) == Decimal("3.87")
assert packed.with_discrepancy == []
assert len(packed.matched) == 1
assert packed.matched[0].osv_entry_ids_a == [source_ids[0]]
assert packed.matched[0].osv_entry_ids_b == [credit_id]
assert packed.matched[0].amount_a == packed.matched[0].amount_b == Decimal("106311116.46")
assert {item.row.entry_ids[0] for item in packed.unpaired} == set(source_ids[1:])
assert {item.reason for item in packed.unpaired} == {"NO_MATCH"}