arm-core/api/tests/test_api_intercompany_matrix.py

613 lines
29 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.

"""Критерии шахматки через HTTP, реальные сервисы и PostgreSQL."""
import uuid
from dataclasses import dataclass
from decimal import Decimal
from io import BytesIO
from urllib.parse import unquote
import pytest
from httpx import AsyncClient, Response
from openpyxl import load_workbook
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.models.access import AppUser
from src.models.accounting import ChartOfAccount, EliminationRule
from src.models.audit_ex import AuditEvent
from src.models.consolidation import ConsolidationDocument, ConsolidationVersion
from src.models.directories import CompanyGroup, LegalEntity, RegionalOffice
from src.models.enums import AuditAction, AuditSection, CounterpartyMatchKind, PeriodQuarter, StatementType
from src.models.osv import OsvEntry
from src.repositories import consolidations as consolidation_repo
from src.repositories import directories as directory_repo
from tests.conftest import AuthClientFactory
from tests.factories import make_company_group, make_confirmed_osv, make_consolidation, make_legal_entity
pytestmark = pytest.mark.integration
OK, FORBIDDEN, NOT_FOUND, UNPROCESSABLE = 200, 403, 404, 422
KA_RF = "ka_rf@armka.local"
RECEIVABLES_PAYABLES_RULE_NO = 6
ROUTES = ("all", "manual", "result-table", "all/export", "manual/export", "result-table/export")
@dataclass
class World:
version: ConsolidationVersion
members: list[LegalEntity]
accounts: dict[str, ChartOfAccount]
@property
def selection(self) -> dict[str, list[str]]:
return {
"debit_account_ids": [str(self.accounts["62.01"].id)],
"credit_account_ids": [str(self.accounts["60.01"].id)],
}
async def _world(session: AsyncSession, *, members: int = 2) -> World:
office = (
await session.scalars(select(RegionalOffice).where(RegionalOffice.name == "Ростовский региональный филиал"))
).one()
user = (await session.scalars(select(AppUser).where(AppUser.email == KA_RF))).one()
group = await make_company_group(session, office=office)
entities = [await make_legal_entity(session, name=f"Компания {n}", group=group) for n in range(members)]
_, version = await make_consolidation(
session, group=group, year=2024, quarter=PeriodQuarter.Q1, members=entities, initiator=user
)
accounts = {account.code: account for account in (await session.scalars(select(ChartOfAccount))).all()}
return World(version, entities, accounts)
async def _request(client: AsyncClient, version_id: uuid.UUID, suffix: str, selection: dict) -> Response:
url = f"/consolidations/versions/{version_id}/intercompany-matrix/{suffix}"
return await client.post(url, json=selection) if suffix.startswith("manual") else await client.get(url)
async def _source(
session: AsyncSession, world: World, *, credit: bool, amount: str, account: str | None = None
) -> uuid.UUID:
owner, counterparty = reversed(world.members) if credit else world.members
code = account or ("60.01" if credit else "62.01")
document = await make_confirmed_osv(
session,
entity=owner,
account=code,
year=2024,
quarter=PeriodQuarter.Q1,
is_cumulative=False,
rows=[
{
"name": counterparty.name,
"counterparty_id": counterparty.id,
"counterparty_match_kind": CounterpartyMatchKind.INN,
"account_id": world.accounts[code].id,
"closing_credit" if credit else "closing_debit": Decimal(amount),
}
],
)
session.add(ConsolidationDocument(consolidation_version_id=world.version.id, document_id=document.id))
await session.flush()
return document.id
@pytest.mark.parametrize("suffix", ["all", "manual"])
async def test_empty_perimeter(auth_client: AuthClientFactory, seeded: AsyncSession, suffix: str) -> None:
world = await _world(seeded, members=0)
response = await _request(await auth_client(KA_RF), world.version.id, suffix, world.selection)
assert response.status_code == OK, response.text
assert response.json() == {"companies": [], "cells": []}
@pytest.mark.parametrize("suffix", ROUTES)
async def test_matrix_reuses_members_and_reads_accounts_through_repository(
auth_client: AuthClientFactory, seeded: AsyncSession, monkeypatch: pytest.MonkeyPatch, suffix: str
) -> None:
world = await _world(seeded)
calls = {"members": 0, "accounts": 0}
original_members = consolidation_repo.members
original_accounts = directory_repo.list_accounts
async def counted_members(*args: object, **kwargs: object):
calls["members"] += 1
return await original_members(*args, **kwargs)
async def counted_accounts(*args: object, **kwargs: object):
calls["accounts"] += 1
return await original_accounts(*args, **kwargs)
monkeypatch.setattr(consolidation_repo, "members", counted_members)
monkeypatch.setattr(directory_repo, "list_accounts", counted_accounts)
response = await _request(await auth_client(KA_RF), world.version.id, suffix, world.selection)
assert response.status_code == OK, response.text
assert calls == {"members": 1, "accounts": 1}
async def test_no_osv_for_selected_accounts(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
world = await _world(seeded)
await _source(seeded, world, credit=False, amount="999", account="58.03")
response = await _request(await auth_client(KA_RF), world.version.id, "manual", world.selection)
assert response.status_code == OK, response.text
assert len(response.json()["cells"]) == len(world.members)
for cell in response.json()["cells"]:
assert {
"osv_entry_ids_debit",
"osv_entry_ids_credit",
"exceeds_threshold",
} <= cell.keys()
assert {"entry_osv_ids_debit", "entry_osv_ids_credit", "is_more_5_percent"}.isdisjoint(cell)
assert cell["amount_a"] == cell["amount_b"] == cell["variance_amount"] == "0.00"
assert cell["variance_percent"] is None
assert cell["one_sided"] is False
assert cell["osv_entry_ids_debit"] == cell["osv_entry_ids_credit"] == []
assert cell["skipped_osv_entry_count"] == 0
async def test_preset_uses_rule_sides_and_exclusions(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
world = await _world(seeded)
rule = (
await seeded.scalars(select(EliminationRule).where(EliminationRule.rule_no == RECEIVABLES_PAYABLES_RULE_NO))
).one()
world.version.elimination_rules_version = rule.version_no
await seeded.flush()
await _source(seeded, world, credit=False, amount="100", account="62.01")
await _source(seeded, world, credit=True, amount="90", account="60.01")
await _source(seeded, world, credit=False, amount="500", account="76.07.5")
await _source(seeded, world, credit=True, amount="400", account="76.07.1")
client = await auth_client(KA_RF)
response = await client.get(f"/consolidations/versions/{world.version.id}/intercompany-matrix/preset/{rule.id}")
assert response.status_code == OK, response.text
cell = next(item for item in response.json()["cells"] if item["company_a_id"] == str(world.members[0].id))
assert cell["amount_a"] == "100.00"
assert cell["amount_b"] == "90.00"
exported = await client.get(
f"/consolidations/versions/{world.version.id}/intercompany-matrix/preset/{rule.id}/export"
)
assert exported.status_code == OK, exported.text
assert f"_rule-balance-{rule.rule_no}.xlsx" in unquote(exported.headers["content-disposition"])
sheet = load_workbook(BytesIO(exported.content))["Шахматка"]
assert sheet["B6"].value == "Пресет"
assert "76.07.5" not in sheet["B7"].value
assert "76.07.1" not in sheet["B8"].value
assert sheet["B9"].value == "Сальдо"
assert sheet["B10"].value == f"ББ № {rule.rule_no}"
assert sheet["B11"].value == rule.article_1_line
assert sheet["B12"].value == rule.article_2_line
assert sheet["B13"].value == f"v{rule.version_no}"
assert Decimal(str(sheet["D16"].value)) == Decimal("100")
assert Decimal(str(sheet["D17"].value)) == Decimal("90")
audit = (await seeded.scalars(select(AuditEvent).where(AuditEvent.action == AuditAction.EXPORT))).one()
assert audit.after_value["mode"] == "PRESET"
assert audit.after_value["rule_id"] == str(rule.id)
assert audit.after_value["rule_no"] == rule.rule_no
assert audit.after_value["rule_version_no"] == rule.version_no
@pytest.mark.parametrize("suffix", ["", "/export"])
async def test_preset_rejects_income_rule(auth_client: AuthClientFactory, seeded: AsyncSession, suffix: str) -> None:
world = await _world(seeded)
rule = (
await seeded.scalars(select(EliminationRule).where(EliminationRule.rule_no == RECEIVABLES_PAYABLES_RULE_NO))
).one()
rule.statement_type = StatementType.income
await seeded.flush()
client = await auth_client(KA_RF)
response = await client.get(
f"/consolidations/versions/{world.version.id}/intercompany-matrix/preset/{rule.id}{suffix}"
)
assert response.status_code == UNPROCESSABLE, response.text
assert response.json()["detail"]["code"] == "INVALID_RULE"
async def test_preset_uses_current_rules_when_version_unfixed(
auth_client: AuthClientFactory, seeded: AsyncSession
) -> None:
world = await _world(seeded)
rule = (
await seeded.scalars(select(EliminationRule).where(EliminationRule.rule_no == RECEIVABLES_PAYABLES_RULE_NO))
).one()
client = await auth_client(KA_RF)
url = f"/consolidations/versions/{world.version.id}/intercompany-matrix/preset/{rule.id}"
listed = await client.get(f"/consolidations/versions/{world.version.id}/elimination-rules")
assert listed.status_code == OK, listed.text
assert listed.json()["is_version_fixed"] is False
assert listed.json()["version_no"] == rule.version_no
assert "as_of" not in listed.json()
assert str(rule.id) in {item["id"] for item in listed.json()["rules"]}
response = await client.get(url)
assert response.status_code == OK, response.text
assert (await client.get(f"{url}/export")).status_code == OK
audit = (await seeded.scalars(select(AuditEvent).where(AuditEvent.action == AuditAction.EXPORT))).one()
assert audit.after_value["rule_no"] == rule.rule_no
assert audit.after_value["rule_version_no"] == rule.version_no
assert world.version.elimination_rules_version is None
world.version.elimination_rules_version = rule.version_no + 1
await seeded.flush()
assert (await client.get(url)).status_code == NOT_FOUND
assert (await client.get(f"{url}/export")).status_code == NOT_FOUND
world.version.elimination_rules_version = rule.version_no
await seeded.flush()
fixed = await client.get(f"/consolidations/versions/{world.version.id}/elimination-rules")
assert fixed.status_code == OK, fixed.text
assert fixed.json()["is_version_fixed"] is True
assert fixed.json()["version_no"] == rule.version_no
missing_url = f"/consolidations/versions/{world.version.id}/intercompany-matrix/preset/{uuid.uuid4()}"
missing = await client.get(missing_url)
assert missing.status_code == NOT_FOUND
assert (await client.get(f"{missing_url}/export")).status_code == NOT_FOUND
async def test_unfixed_rules_choose_latest_open_version(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
world = await _world(seeded)
old_rule, new_rule = (
await seeded.scalars(select(EliminationRule).order_by(EliminationRule.rule_no).limit(2))
).all()
assert old_rule.valid_to is None and new_rule.valid_to is None
new_rule.rule_no = old_rule.rule_no
new_rule.version_no = old_rule.version_no + 1
await seeded.flush()
client = await auth_client(KA_RF)
base = f"/consolidations/versions/{world.version.id}"
response = await client.get(f"{base}/elimination-rules")
assert response.status_code == OK, response.text
payload = response.json()
assert payload["is_version_fixed"] is False
assert payload["version_no"] == new_rule.version_no
assert [item["id"] for item in payload["rules"]] == [str(new_rule.id)]
assert payload["rules"][0]["rule_no"] == old_rule.rule_no
old_preset = await client.get(f"{base}/intercompany-matrix/preset/{old_rule.id}")
assert old_preset.status_code == NOT_FOUND
async def test_elimination_pairs_use_preset_account_selection( # noqa: PLR0915
auth_client: AuthClientFactory, seeded: AsyncSession
) -> None:
world = await _world(seeded, members=3)
rule_ids = {
rule.rule_no: rule.id
for rule in (await seeded.scalars(select(EliminationRule).where(EliminationRule.rule_no.in_([1, 3, 6])))).all()
}
debit_document = await make_confirmed_osv(
seeded,
entity=world.members[0],
account="62.01",
year=2024,
quarter=PeriodQuarter.Q1,
is_cumulative=True,
rows=[
{
"name": member.name,
"counterparty_id": member.id,
"account_id": world.accounts["62.01"].id,
"closing_debit": Decimal(amount),
}
for member, amount in [
*((world.members[1], amount) for amount in ("40", "0", "200")),
(world.members[2], "103"),
]
],
)
second_debit_document = await make_confirmed_osv(
seeded,
entity=world.members[0],
account="60.02",
year=2024,
quarter=PeriodQuarter.Q1,
is_cumulative=True,
rows=[
{
"name": world.members[1].name,
"counterparty_id": world.members[1].id,
"account_id": world.accounts["60.02"].id,
"closing_debit": Decimal("60"),
}
],
)
credit_document = await make_confirmed_osv(
seeded,
entity=world.members[1],
account="60.01",
year=2024,
quarter=PeriodQuarter.Q1,
is_cumulative=True,
rows=[
{
"name": world.members[0].name,
"counterparty_id": world.members[0].id,
"account_id": world.accounts["60.01"].id,
"closing_credit": Decimal("100"),
}
],
)
other_credit_document = await make_confirmed_osv(
seeded,
entity=world.members[2],
account="60.01",
year=2024,
quarter=PeriodQuarter.Q1,
is_cumulative=True,
rows=[
{
"name": world.members[0].name,
"counterparty_id": world.members[0].id,
"account_id": world.accounts["60.01"].id,
"closing_credit": Decimal("100"),
}
],
)
seeded.add_all(
[
ConsolidationDocument(consolidation_version_id=world.version.id, document_id=document.id)
for document in (debit_document, second_debit_document, credit_document, other_credit_document)
]
)
await seeded.flush()
client = await auth_client(KA_RF)
url = f"/consolidations/versions/{world.version.id}/elimination-pairs"
company_a_id, company_b_id, company_c_id = (member.id for member in world.members)
params = {"rule_id": str(rule_ids[6]), "company_a_id": str(company_a_id), "company_b_id": str(company_b_id)}
response = await client.get(url, params=params)
assert response.status_code == OK, response.text
payload = response.json()
assert payload["version_no"] == 1
assert (payload["rule_id"], payload["rule_no"], payload["company_a_id"], payload["company_b_id"]) == (
str(rule_ids[6]),
6,
str(company_a_id),
str(company_b_id),
)
assert "account_pairs" not in payload
assert payload["with_discrepancy"] == []
assert len(payload["matched"]) == 1
pair = payload["matched"][0]
assert len(pair["osv_entry_ids_a"]) == len((debit_document, second_debit_document))
assert len(pair["osv_entry_ids_b"]) == 1
assert pair["rule_id"] == str(rule_ids[6])
assert tuple(Decimal(pair[field]) for field in ("amount_a", "amount_b", "elimination_amount")) == (
Decimal("100"),
Decimal("100"),
Decimal("0"),
)
assert "article_ids_a" not in pair and "article_ids_b" not in pair
assert "deviation_amount" not in pair
assert Decimal(pair["deviation_percent"]) == 0
assert [item["reason"] for item in payload["unpaired"]] == ["ZERO_AMOUNT", "NO_MATCH"]
loans = await client.get(url, params={**params, "rule_id": str(rule_ids[3])})
assert loans.status_code == OK, loans.text
assert loans.json()["matched"] == []
manual = await client.get(url, params={**params, "rule_id": str(rule_ids[1])})
assert manual.status_code == OK, manual.text
assert manual.json()["matched"] == []
other_pair = await client.get(url, params={**params, "company_b_id": str(company_c_id)})
assert other_pair.status_code == OK, other_pair.text
assert other_pair.json()["matched"] == []
assert len(other_pair.json()["with_discrepancy"]) == 1
discrepant = other_pair.json()["with_discrepancy"][0]
assert (Decimal(discrepant["amount_a"]), Decimal(discrepant["amount_b"])) == (Decimal("103"), Decimal("100"))
assert Decimal(discrepant["elimination_amount"]) == Decimal("3")
assert "article_ids_a" not in discrepant and "article_ids_b" not in discrepant
assert "deviation_amount" not in discrepant
reversed_pair = await client.get(
url, params={**params, "company_a_id": str(company_b_id), "company_b_id": str(company_a_id)}
)
assert reversed_pair.status_code == OK, reversed_pair.text
assert reversed_pair.json()["matched"] == []
assert reversed_pair.json()["with_discrepancy"] == []
assert reversed_pair.json()["unpaired"] == []
invalid_pair = await client.get(url, params={**params, "company_b_id": str(uuid.uuid4())})
assert invalid_pair.status_code == UNPROCESSABLE
same_company = await client.get(url, params={**params, "company_b_id": str(company_a_id)})
assert same_company.status_code == UNPROCESSABLE
missing_rule = await client.get(url, params={**params, "rule_id": str(uuid.uuid4())})
assert missing_rule.status_code == NOT_FOUND
missing_parameters = await client.get(url)
assert missing_parameters.status_code == UNPROCESSABLE
missing = await client.get(f"/consolidations/versions/{uuid.uuid4()}/elimination-pairs", params=params)
assert missing.status_code == NOT_FOUND
async def test_all_excludes_off_balance_accounts_but_manual_can_select_them(
auth_client: AuthClientFactory, seeded: AsyncSession
) -> None:
world = await _world(seeded)
assert world.accounts["001"].is_off_balance is True
await _source(seeded, world, credit=False, amount="100", account="001")
client = await auth_client(KA_RF)
all_response = await _request(client, world.version.id, "all", world.selection)
assert all_response.status_code == OK, all_response.text
assert all(Decimal(cell["amount_a"]) == 0 for cell in all_response.json()["cells"])
manual_selection = {
"debit_account_ids": [str(world.accounts["001"].id)],
"credit_account_ids": [str(world.accounts["60.01"].id)],
}
manual_response = await _request(client, world.version.id, "manual", manual_selection)
assert manual_response.status_code == OK, manual_response.text
cell = next(c for c in manual_response.json()["cells"] if c["company_a_id"] == str(world.members[0].id))
assert Decimal(cell["amount_a"]) == Decimal("100")
async def test_unmapped_account_source_is_visible_in_cell_count(
auth_client: AuthClientFactory, seeded: AsyncSession
) -> None:
world = await _world(seeded)
document_id = await _source(seeded, world, credit=False, amount="100")
entry = (await seeded.scalars(select(OsvEntry).where(OsvEntry.document_id == document_id))).one()
entry.account_id = None
await seeded.flush()
await _source(seeded, world, credit=True, amount="80")
response = await _request(await auth_client(KA_RF), world.version.id, "manual", world.selection)
assert response.status_code == OK, response.text
cells = response.json()["cells"]
assert [cell["skipped_osv_entry_count"] for cell in cells] == [1, 1]
cell = next(c for c in cells if c["company_a_id"] == str(world.members[0].id))
assert cell["amount_a"] == "0.00"
assert cell["amount_b"] == "80.00"
assert cell["osv_entry_ids_debit"] == []
assert len(cell["osv_entry_ids_credit"]) == 1
@pytest.mark.parametrize("credit", [False, True])
@pytest.mark.parametrize("amount", ["0", "100.25"])
async def test_pair_without_counterpart(
auth_client: AuthClientFactory, seeded: AsyncSession, credit: bool, amount: str
) -> None:
world = await _world(seeded)
await _source(seeded, world, credit=credit, amount=amount)
response = await _request(await auth_client(KA_RF), world.version.id, "manual", world.selection)
assert response.status_code == OK, response.text
cell = next(c for c in response.json()["cells"] if c["company_a_id"] == str(world.members[0].id))
assert Decimal(cell["amount_a"]) == (Decimal(0) if credit else Decimal(amount))
assert Decimal(cell["amount_b"]) == (Decimal(amount) if credit else Decimal(0))
assert cell["one_sided"] is (Decimal(amount) != 0)
assert cell["variance_percent"] is None
assert cell["exceeds_threshold"] is False
assert bool(cell["osv_entry_ids_credit"]) is credit
assert bool(cell["osv_entry_ids_debit"]) is not credit
@pytest.mark.parametrize("suffix", ROUTES)
async def test_visibility_and_permission(auth_client: AuthClientFactory, seeded: AsyncSession, suffix: str) -> None:
world = await _world(seeded)
foreign = await auth_client("client_manager@armka.local")
hidden = await _request(foreign, world.version.id, suffix, world.selection)
unknown = await _request(foreign, uuid.uuid4(), suffix, world.selection)
assert hidden.status_code == unknown.status_code == NOT_FOUND
assert hidden.json() == unknown.json()
forbidden = await _request(await auth_client("admin@armka.local"), world.version.id, suffix, world.selection)
assert forbidden.status_code == FORBIDDEN
assert "CONSOLIDATION_VIEW" in forbidden.json()["detail"]
@pytest.mark.parametrize(("debit", "credit", "percent"), [("8890", "8000", "11.13"), ("105001", "100000", "5.00")])
async def test_rounded_percent_in_api(
auth_client: AuthClientFactory, seeded: AsyncSession, debit: str, credit: str, percent: str
) -> None:
world = await _world(seeded)
await _source(seeded, world, credit=False, amount=debit)
await _source(seeded, world, credit=True, amount=credit)
response = await _request(await auth_client(KA_RF), world.version.id, "manual", world.selection)
assert response.status_code == OK, response.text
cell = next(c for c in response.json()["cells"] if c["company_a_id"] == str(world.members[0].id))
assert cell["variance_percent"] == percent
assert cell["exceeds_threshold"] is True
assert cell["one_sided"] is False
@pytest.mark.parametrize("suffix", ["manual", "manual/export"])
@pytest.mark.parametrize("side", ["debit_account_ids", "credit_account_ids"])
@pytest.mark.parametrize("code", ["ACCOUNT_NOT_FOUND", "ACCOUNT_KIND_MISMATCH"])
async def test_invalid_account_selection(
auth_client: AuthClientFactory, seeded: AsyncSession, suffix: str, side: str, code: str
) -> None:
world = await _world(seeded)
selection = world.selection
wrong_kind = world.accounts["80" if side == "debit_account_ids" else "01"]
selection[side] = [str(uuid.uuid4() if code == "ACCOUNT_NOT_FOUND" else wrong_kind.id)]
response = await _request(await auth_client(KA_RF), world.version.id, suffix, selection)
assert response.status_code == UNPROCESSABLE, response.text
assert response.json()["detail"]["code"] == code
@pytest.mark.parametrize("suffix", ["result-table", "result-table/export"])
async def test_result_accounts_missing(auth_client: AuthClientFactory, seeded: AsyncSession, suffix: str) -> None:
world = await _world(seeded)
# Меняем код в тестовой транзакции, сохраняя FK существующих правил и потомков.
world.accounts["58.03"].code = "58.03-missing"
await seeded.flush()
response = await _request(await auth_client(KA_RF), world.version.id, suffix, world.selection)
assert response.status_code == UNPROCESSABLE, response.text
assert response.json()["detail"]["code"] == "RESULT_ACCOUNTS_MISSING"
assert "58.03" in response.json()["detail"]["message"]
assert not (await seeded.scalars(select(AuditEvent).where(AuditEvent.action == AuditAction.EXPORT))).all()
async def test_result_table_export_matches_json(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
world = await _world(seeded)
await _source(seeded, world, credit=False, amount="100.25")
client = await auth_client(KA_RF)
result = await _request(client, world.version.id, "result-table", world.selection)
export = await _request(client, world.version.id, "result-table/export", world.selection)
assert result.status_code == export.status_code == OK
data = result.json()
sheet = load_workbook(BytesIO(export.content))["Результирующая таблица"]
assert [sheet.cell(7, col).value for col in range(2, len(data["companies"]) + 2)] == [
company["name"] for company in data["companies"]
]
for row, article in enumerate(data["articles"], start=8):
assert sheet.cell(row, 1).value == article["name"]
amounts = {item["company_id"]: Decimal(item["amount"]) for item in article["amounts"]}
for col, company in enumerate(data["companies"], start=2):
assert Decimal(str(sheet.cell(row, col).value)) == amounts[company["id"]]
assert Decimal(str(sheet.cell(row, len(data["companies"]) + 2).value)) == Decimal(article["group_total"])
event = (await seeded.scalars(select(AuditEvent).where(AuditEvent.action == AuditAction.EXPORT))).one()
assert event.after_value == {"report": "intercompany_result_table", "articles": len(data["articles"])}
@pytest.mark.parametrize("mode", ["all", "manual"])
async def test_export_filename_header_and_percentage(
auth_client: AuthClientFactory, seeded: AsyncSession, mode: str
) -> None:
world = await _world(seeded)
await _source(seeded, world, credit=False, amount="100")
await _source(seeded, world, credit=True, amount="90")
group_name = (
await seeded.scalars(select(CompanyGroup.name).where(CompanyGroup.id == world.members[0].company_group_id))
).one()
response = await _request(await auth_client(KA_RF), world.version.id, f"{mode}/export", world.selection)
assert response.status_code == OK, response.text
disposition = response.headers["content-disposition"]
assert unquote(disposition.split("filename*=UTF-8''", 1)[1]) == (
f"intercompany-matrix_{group_name}_2024-Q1_v1.xlsx"
)
workbook = load_workbook(BytesIO(response.content))
sheet = workbook["Шахматка"]
assert [sheet[f"B{row}"].value for row in range(2, 9)] == [
group_name,
"2024-Q1",
"v1",
"руб.; расхождение — %",
"Выбранные счета" if mode == "manual" else "Все балансовые счета",
"62.01" if mode == "manual" else "Все балансовые счета",
"60.01" if mode == "manual" else "Все балансовые счета",
]
assert sheet["B14"].value == "Расхождение, %"
expected_percent = Decimal("11.11")
assert Decimal(str(sheet["D14"].value)) == expected_percent
assert sheet["D14"].number_format == '0.00"%"'
@pytest.mark.parametrize("mode", ["all", "manual"])
async def test_export_appends_audit(auth_client: AuthClientFactory, seeded: AsyncSession, mode: str) -> None:
world = await _world(seeded)
client = await auth_client(KA_RF)
query = select(AuditEvent).where(AuditEvent.action == AuditAction.EXPORT)
for _ in range(2):
response = await _request(client, world.version.id, f"{mode}/export", world.selection)
assert response.status_code == OK, response.text
events = (await seeded.scalars(query)).all()
assert len(events) == 2 # noqa: PLR2004 - отдельное событие на каждую выгрузку
assert events[0].id != events[1].id
for event in events:
assert event.section == AuditSection.EXPORT
assert event.object_type == "consolidation_version"
assert event.object_id == event.consolidation_version_id == world.version.id
assert event.user_id is not None and event.before_value is None
assert event.after_value == {
"report": "intercompany_matrix",
"mode": mode.upper(),
"cells": len(world.members),
**(world.selection if mode == "manual" else {"debit_account_ids": [], "credit_account_ids": []}),
}