From 38c8eefa184c9e57f04d6a3e72af5c36cefc63d3 Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Wed, 21 Oct 2026 18:43:19 +0300 Subject: [PATCH] =?UTF-8?q?fix(matrix):=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=87=D0=B8=D1=82=D1=8C=20PRESET=20=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=D0=B0=D0=BC=D0=B8=20=D0=91=D0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/services/intercompany_matrix.py | 4 +++- api/tests/test_api_intercompany_matrix.py | 19 +++++++++++++++++- api/tests/test_intercompany_matrix.py | 20 ++++++++++++++++++- .../specs/consolidated-osv/spec.md | 6 +++++- .../intercompany-matrix-presets/tasks.md | 2 +- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/api/src/services/intercompany_matrix.py b/api/src/services/intercompany_matrix.py index c77fe55..ae8c6bf 100644 --- a/api/src/services/intercompany_matrix.py +++ b/api/src/services/intercompany_matrix.py @@ -16,7 +16,7 @@ from src.domain.policy import VisibilityScope from src.models.accounting import ChartOfAccount, EliminationRule from src.models.consolidation import Consolidation, ConsolidationVersion from src.models.directories import CompanyGroup -from src.models.enums import AccountKind, PeriodQuarter +from src.models.enums import AccountKind, PeriodQuarter, StatementType from src.repositories import directories as directory_repo from src.repositories import elimination_rules as elimination_rules_repo from src.schemas.consolidations import ( @@ -419,6 +419,8 @@ class IntercompanyMatrixService: rule, rule_accounts = await elimination_rules_repo.rule_with_account(self._session, rule_id) if rule is None or not elimination_rules_repo.is_valid_for_version(rule, version.elimination_rules_version): raise NotFoundError("правило-пресет не найдено для версии консолидации") + if rule.statement_type != StatementType.balance: + raise DomainValidationError("INVALID_RULE", "Шахматка PRESET поддерживает только правила ББ") if rule.side_1 not in ("D", "K") or rule.measure not in ("closing", "turnover"): raise DomainValidationError("INVALID_RULE", "Неверное направление или показатель правила-пресета") diff --git a/api/tests/test_api_intercompany_matrix.py b/api/tests/test_api_intercompany_matrix.py index 0f49a82..34607b3 100644 --- a/api/tests/test_api_intercompany_matrix.py +++ b/api/tests/test_api_intercompany_matrix.py @@ -17,7 +17,7 @@ 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 +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 @@ -179,6 +179,23 @@ async def test_preset_uses_rule_sides_and_exclusions(auth_client: AuthClientFact assert audit.after_value["rule_id"] == str(rule.id) +@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_unpaired_osv_returns_rows_and_total(auth_client: AuthClientFactory, seeded: AsyncSession) -> None: world = await _world(seeded) rule = ( diff --git a/api/tests/test_intercompany_matrix.py b/api/tests/test_intercompany_matrix.py index b9e91b1..7aa7b1d 100644 --- a/api/tests/test_intercompany_matrix.py +++ b/api/tests/test_intercompany_matrix.py @@ -7,9 +7,10 @@ from types import SimpleNamespace from unittest.mock import AsyncMock import pytest +from fastapi import status as http_status from src.api.endpoints.consolidations.intercompany_matrix import router -from src.models.enums import AccountKind +from src.models.enums import AccountKind, StatementType from src.schemas.consolidations import ( IntercompanyMatrixCellOut, IntercompanyMatrixCompanyOut, @@ -128,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(), + statement_type=StatementType.balance, version_no=1, side_1="D", measure="turnover", @@ -162,6 +164,22 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey assert info.measure == "turnover" +async def test_preset_rejects_income_rule_before_selecting_accounts(monkeypatch: pytest.MonkeyPatch) -> None: + rule = SimpleNamespace( + statement_type=StatementType.income, + version_no=1, + valid_to=None, + ) + monkeypatch.setattr(matrix.elimination_rules_repo, "rule_with_account", AsyncMock(return_value=(rule, []))) + service = matrix.IntercompanyMatrixService(object(), None, object()) + + with pytest.raises(matrix.DomainValidationError) as error: + await service._resolve_preset_selection(uuid.uuid4(), SimpleNamespace(elimination_rules_version=1), {}, {}) + + assert error.value.status == http_status.HTTP_422_UNPROCESSABLE_CONTENT + assert error.value.code == "INVALID_RULE" + + def test_one_sided_reflects_osv_sources_even_when_amount_is_zero() -> None: company_a, company_b = uuid.uuid4(), uuid.uuid4() debit_account, credit_account = uuid.uuid4(), uuid.uuid4() diff --git a/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md b/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md index e168e46..6c5d008 100644 --- a/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md +++ b/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md @@ -9,7 +9,7 @@ `closing` — конечное сальдо (`closing_debit`/`closing_credit`), `turnover` — обороты (`turnover_debit`/`turnover_credit`). Правило SHALL быть действительным для версии консолидации: при заданном `elimination_rules_version` — с тем же `version_no`, иначе — без `valid_to`; несуществующее или недействительное правило SHALL давать 404. Ошибки 422: -`RULE_REQUIRED` — правило не выбрано, `INVALID_RULE` — неверные `side_1`, `measure` или сторона счёта правила, +`RULE_REQUIRED` — правило не выбрано, `INVALID_RULE` — правило ОФР либо неверные `side_1`, `measure` или сторона счёта правила, `ACCOUNTS_REQUIRED` — у одной из сторон нет счетов. Невидимая и несуществующая версия SHALL отвечать 404 `NOT_FOUND`. #### Scenario: Пресет по сальдо @@ -28,6 +28,10 @@ - **WHEN** `rule_id` не существует - **THEN** 404 +#### Scenario: Правило ОФР +- **WHEN** у правила `statement_type = income` +- **THEN** расчёт и выгрузка возвращают 422 `INVALID_RULE` + ### Requirement: Строки сводной ОСВ без пары по пресету `GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}/unpaired-osv` (`CONSOLIDATION_VIEW`, policy) SHALL возвращать `{items, total}`: все строки сводной ОСВ версии, чьи записи ОСВ diff --git a/openspec/changes/intercompany-matrix-presets/tasks.md b/openspec/changes/intercompany-matrix-presets/tasks.md index e2bd75c..5a60294 100644 --- a/openspec/changes/intercompany-matrix-presets/tasks.md +++ b/openspec/changes/intercompany-matrix-presets/tasks.md @@ -9,6 +9,6 @@ `correction_ids_*`) вместе с тестами; код выделен в `AURORA-1747`. - [ ] 2.1a Убрать ручку `unpaired-osv` (`get_unpaired_rows`, `UnpairedOsvOut`, маршрут) вместе с тестами; код сохранён в `wip/shahmatka-corrections-unpaired`. -- [ ] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест. +- [x] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест. - [ ] 2.3 Несколько открытых `version_no` — одна версия (max) или явная ошибка, тест. - [ ] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README.