From b0789334286e1d4db683aeb865ec11d7bf3c14b0 Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Wed, 21 Oct 2026 18:54:58 +0300 Subject: [PATCH] =?UTF-8?q?fix(audit):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B8=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8E=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20=D0=B2=20=D1=8D=D0=BA=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82=D0=B5=20=D0=BF=D1=80=D0=B5=D1=81=D0=B5?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- .../endpoints/consolidations/intercompany_matrix.py | 12 +++++++++++- api/src/services/intercompany_matrix.py | 11 +++++++++-- api/tests/test_api_intercompany_matrix.py | 5 +++++ api/tests/test_intercompany_matrix.py | 3 +++ api/tests/test_intercompany_matrix_export.py | 5 ++++- .../changes/intercompany-matrix-presets/design.md | 3 +-- .../changes/intercompany-matrix-presets/proposal.md | 2 +- .../specs/audit-trail/spec.md | 5 +++-- 9 files changed, 39 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b775d9f..f40532f 100644 --- a/README.md +++ b/README.md @@ -876,7 +876,8 @@ API: список счетов `GET /directories/accounts`, правила ве `GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной версии правил — открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ сообщает этот номер. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`, `ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. В шапке xlsx — строка «Показатель», -audit `EXPORT` пишет `mode` (`ALL`/`MANUAL`/`PRESET`) и `rule_id`. Открытые вопросы по пресетам — +audit `EXPORT` пишет `mode` (`ALL`/`MANUAL`/`PRESET`), для PRESET — `rule_id`, `rule_no` и фактический +`rule_version_no`. Открытые вопросы по пресетам — `openspec/changes/intercompany-matrix-presets/design.md`. Контракт для фронта — `../docs/dev/frontend-api-guide.md` §5.8; требования — в существующих спецификациях `openspec/specs/{directories-api,consolidations-api,consolidated-osv,audit-trail}/spec.md`. diff --git a/api/src/api/endpoints/consolidations/intercompany_matrix.py b/api/src/api/endpoints/consolidations/intercompany_matrix.py index 96f61e7..d4645ca 100644 --- a/api/src/api/endpoints/consolidations/intercompany_matrix.py +++ b/api/src/api/endpoints/consolidations/intercompany_matrix.py @@ -52,6 +52,8 @@ async def _export_response( *, selection: IntercompanyMatrixManualIn | None = None, ) -> Response: + if info.mode == "PRESET" and (info.rule_id is None or info.rule_no is None or info.rule_version_no is None): + raise ValueError("Для аудита PRESET нужны ID, номер и версия правила") response = await _xlsx_response(matrix, info) await audit.record( action=AuditAction.EXPORT, @@ -64,7 +66,15 @@ async def _export_response( "mode": info.mode, "debit_account_ids": [str(account_id) for account_id in selection.debit_account_ids] if selection else [], "credit_account_ids": [str(account_id) for account_id in selection.credit_account_ids] if selection else [], - **({"rule_id": str(info.rule_id)} if info.rule_id is not None else {}), + **( + { + "rule_id": str(info.rule_id), + "rule_no": info.rule_no, + "rule_version_no": info.rule_version_no, + } + if info.mode == "PRESET" + else {} + ), "cells": len(matrix.cells), }, ) diff --git a/api/src/services/intercompany_matrix.py b/api/src/services/intercompany_matrix.py index 5d6ba47..29b8cac 100644 --- a/api/src/services/intercompany_matrix.py +++ b/api/src/services/intercompany_matrix.py @@ -87,6 +87,7 @@ class MatrixBuildResult: accounts: list[ChartOfAccount] selection: MatrixSelection rows: tuple[ConsolidatedRow, ...] + rule: EliminationRule | None @dataclass(frozen=True) @@ -100,6 +101,8 @@ class MatrixExportInfo: credit_account_codes: tuple[str, ...] measure: Measure = "closing" rule_id: uuid.UUID | None = None + rule_no: int | None = None + rule_version_no: int | None = None @property def filename(self) -> str: @@ -394,7 +397,9 @@ class IntercompanyMatrixService: debit_account_codes=debit_codes, credit_account_codes=credit_codes, measure=result.selection.measure, - rule_id=rule_id, + rule_id=result.rule.id if result.rule is not None else None, + rule_no=result.rule.rule_no if result.rule is not None else None, + rule_version_no=result.rule.version_no if result.rule is not None else None, ) return result.matrix, info @@ -462,6 +467,7 @@ class IntercompanyMatrixService: rows = osv_result.consolidated.rows accounts = await directory_repo.list_accounts(self._session) by_id, children = _make_account_tree(accounts) + rule: EliminationRule | None = None if mode == "ALL": if debit_account_ids or credit_account_ids: raise DomainValidationError("ACCOUNTS_NOT_ALLOWED", "Для режима ALL не нужно указывать счета") @@ -475,7 +481,7 @@ class IntercompanyMatrixService: credit = _select_accounts(credit_account_ids, (), "K", by_id, children) selection = MatrixSelection(debit, credit, "closing") elif mode == "PRESET": - selection, _ = await self._resolve_preset_selection(rule_id, osv_result.version, by_id, children) + selection, rule = await self._resolve_preset_selection(rule_id, osv_result.version, by_id, children) else: raise DomainValidationError("INVALID_MODE", "Неизвестный режим выбора счетов") matrix = self._calculate(rows, osv_result.members, selection) @@ -486,4 +492,5 @@ class IntercompanyMatrixService: accounts=accounts, selection=selection, rows=rows, + rule=rule, ) diff --git a/api/tests/test_api_intercompany_matrix.py b/api/tests/test_api_intercompany_matrix.py index 77da4a4..c56f9cc 100644 --- a/api/tests/test_api_intercompany_matrix.py +++ b/api/tests/test_api_intercompany_matrix.py @@ -177,6 +177,8 @@ async def test_preset_uses_rule_sides_and_exclusions(auth_client: AuthClientFact 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"]) @@ -241,6 +243,9 @@ async def test_preset_uses_current_rules_when_version_unfixed( 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() diff --git a/api/tests/test_intercompany_matrix.py b/api/tests/test_intercompany_matrix.py index 2b2936d..2132df7 100644 --- a/api/tests/test_intercompany_matrix.py +++ b/api/tests/test_intercompany_matrix.py @@ -129,6 +129,7 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey monkeypatch.setattr(matrix.directory_repo, "list_accounts", AsyncMock(return_value=[parent, child])) rule = SimpleNamespace( id=uuid.uuid4(), + rule_no=6, statement_type=StatementType.balance, version_no=1, side_1="D", @@ -165,6 +166,8 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey assert info.debit_account_codes == ("76",) assert info.credit_account_codes == ("76", "76.01") assert info.measure == "turnover" + assert info.rule_no == rule.rule_no + assert info.rule_version_no == rule.version_no async def test_preset_rejects_income_rule_before_selecting_accounts(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/test_intercompany_matrix_export.py b/api/tests/test_intercompany_matrix_export.py index 1b78373..0a53491 100644 --- a/api/tests/test_intercompany_matrix_export.py +++ b/api/tests/test_intercompany_matrix_export.py @@ -37,6 +37,8 @@ def _info(*, mode: str = "MANUAL", rule_id: UUID | None = None) -> MatrixExportI credit_account_codes=("60.01",) if mode != "ALL" else (), measure="turnover" if mode == "PRESET" else "closing", rule_id=rule_id, + rule_no=6 if mode == "PRESET" else None, + rule_version_no=3 if mode == "PRESET" else None, ) @@ -135,7 +137,6 @@ def test_matrix_export_has_three_rows_per_debit_company_and_highlights_threshold ) - async def test_export_routes_use_matrix_calculation() -> None: version_id = UUID("11111111-1111-1111-1111-111111111111") account_id = UUID("22222222-2222-2222-2222-222222222222") @@ -234,6 +235,8 @@ async def test_export_routes_use_matrix_calculation() -> None: "debit_account_ids": [], "credit_account_ids": [], "rule_id": str(account_id), + "rule_no": 6, + "rule_version_no": 3, "cells": 1, }, }, diff --git a/openspec/changes/intercompany-matrix-presets/design.md b/openspec/changes/intercompany-matrix-presets/design.md index d08da89..cd983a2 100644 --- a/openspec/changes/intercompany-matrix-presets/design.md +++ b/openspec/changes/intercompany-matrix-presets/design.md @@ -51,8 +51,7 @@ change не входит. Шахматка — чистая функция от ## Known issues -1. Имя файла содержит UUID правила, шапка xlsx не содержит номер правила и версию; audit EXPORT не пишет номер и - версию правил. +1. Имя файла содержит UUID правила, шапка xlsx не содержит номер правила и версию. ## Decisions made without discussion diff --git a/openspec/changes/intercompany-matrix-presets/proposal.md b/openspec/changes/intercompany-matrix-presets/proposal.md index 4c29410..25ab009 100644 --- a/openspec/changes/intercompany-matrix-presets/proposal.md +++ b/openspec/changes/intercompany-matrix-presets/proposal.md @@ -15,7 +15,7 @@ `id`. Для версии без зафиксированного `elimination_rules_version` отдаются открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ содержит этот номер. - xlsx: для PRESET имя файла с `_preset-`, в шапке строка «Показатель»; audit `EXPORT` пишет `mode` - `ALL`/`MANUAL`/`PRESET` и `rule_id`. + `ALL`/`MANUAL`/`PRESET`, а для PRESET также `rule_id`, `rule_no` и фактический `rule_version_no`. ## Capabilities diff --git a/openspec/changes/intercompany-matrix-presets/specs/audit-trail/spec.md b/openspec/changes/intercompany-matrix-presets/specs/audit-trail/spec.md index 838a8d1..a38ff01 100644 --- a/openspec/changes/intercompany-matrix-presets/specs/audit-trail/spec.md +++ b/openspec/changes/intercompany-matrix-presets/specs/audit-trail/spec.md @@ -6,7 +6,8 @@ SHALL добавлять отдельное неизменяемое событие `audit_event` с `action = EXPORT`, `section = EXPORT`, `object_type = consolidation_version`, `object_id = consolidation_version_id`, `user_id` инициатора, `before_value = null` и `after_value = {report: "intercompany_matrix", mode, debit_account_ids, -credit_account_ids, cells}`; для `PRESET` в `after_value` SHALL быть дополнительно `rule_id`. `mode` — `ALL`, +credit_account_ids, cells}`; для `PRESET` в `after_value` SHALL быть дополнительно `rule_id`, `rule_no` и +`rule_version_no` (фактическая версия применённого правила, в том числе при незафиксированной версии правил). `mode` — `ALL`, `MANUAL` или `PRESET`, массивы счетов — ID из запроса (в ALL и PRESET пустые), `cells` — число ячеек. Выгрузка сводной ОСВ продолжает писать собственное событие `EXPORT`. @@ -16,4 +17,4 @@ credit_account_ids, cells}`; для `PRESET` в `after_value` SHALL быть д #### Scenario: Выгрузка пресета - **WHEN** пользователь выгружает `GET …/intercompany-matrix/preset/{rule_id}/export` -- **THEN** событие `EXPORT` содержит `mode = PRESET`, `rule_id` правила, пустые массивы счетов и число ячеек +- **THEN** событие `EXPORT` содержит `mode = PRESET`, `rule_id`, `rule_no`, `rule_version_no`, пустые массивы счетов и число ячеек