diff --git a/README.md b/README.md index 928bfcc..9b9eb3e 100644 --- a/README.md +++ b/README.md @@ -871,8 +871,7 @@ Q2–Q4 не подходят (R-14a). Привязываются только ` API: список счетов `GET /directories/accounts`, правила версии `GET …/elimination-rules`, матрица `GET …/intercompany-matrix/all` и `POST …/manual`, их XLSX-выгрузки, статичная таблица `GET …/intercompany-matrix/result-table`. Режим PRESET (счета и показатель из правила элиминации): -`GET …/intercompany-matrix/preset/{rule_id}`, его выгрузка `…/preset/{rule_id}/export` и строки сводной ОСВ без пары -`…/preset/{rule_id}/unpaired-osv`. +`GET …/intercompany-matrix/preset/{rule_id}` и его выгрузка `…/preset/{rule_id}/export`. `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 пресета оканчивается на diff --git a/api/src/api/endpoints/consolidations/intercompany_matrix.py b/api/src/api/endpoints/consolidations/intercompany_matrix.py index 60dd321..6e80018 100644 --- a/api/src/api/endpoints/consolidations/intercompany_matrix.py +++ b/api/src/api/endpoints/consolidations/intercompany_matrix.py @@ -16,7 +16,6 @@ from src.schemas.consolidations import ( IntercompanyMatrixManualIn, IntercompanyMatrixOut, IntercompanyResultTableOut, - UnpairedOsvOut, ) from src.services.intercompany_matrix import MatrixExportInfo from src.services.intercompany_matrix_export import build_matrix_workbook @@ -177,22 +176,6 @@ async def build_intercompany_matrix_preset( return await service.build_accounts(version_id, mode="PRESET", rule_id=rule_id) -@router.get( - "/{version_id}/intercompany-matrix/preset/{rule_id}/unpaired-osv", - response_model=UnpairedOsvOut, - dependencies=VIEW, - responses=ERRORS, - summary="Строки сводной ОСВ без пары по правилу-пресету", -) -async def get_unpaired_osv( - version_id: uuid.UUID, - rule_id: uuid.UUID, - service: IntercompanyMatrixServiceDep, -) -> UnpairedOsvOut: - """Строки свода односторонних ячеек шахматки §6 и их число.""" - return await service.get_unpaired_osv(version_id, rule_id) - - @router.get( "/{version_id}/intercompany-matrix/preset/{rule_id}/export", response_class=Response, diff --git a/api/src/schemas/consolidations.py b/api/src/schemas/consolidations.py index 00435a7..d3863d7 100644 --- a/api/src/schemas/consolidations.py +++ b/api/src/schemas/consolidations.py @@ -232,13 +232,6 @@ class ConsolidatedRowOut(BaseModel): return None if value is None else value.quantize(CENT) -class UnpairedOsvOut(BaseModel): - """Все строки сводной ОСВ без встречной стороны по правилу и их количество.""" - - items: list[ConsolidatedRowOut] - total: int - - class GapOut(BaseModel): """Пробел свода: у ЮЛ по счёту нет квартальных ОСВ за перечисленные кварталы (строки по нему не строятся).""" diff --git a/api/src/services/intercompany_matrix.py b/api/src/services/intercompany_matrix.py index 2e99a99..9becb76 100644 --- a/api/src/services/intercompany_matrix.py +++ b/api/src/services/intercompany_matrix.py @@ -26,9 +26,8 @@ from src.schemas.consolidations import ( IntercompanyResultAmountOut, IntercompanyResultArticleOut, IntercompanyResultTableOut, - UnpairedOsvOut, ) -from src.services.consolidated_osv import ConsolidatedOsvService, row_out +from src.services.consolidated_osv import ConsolidatedOsvService Measure = Literal["closing", "turnover"] Side = Literal["D", "K"] @@ -86,7 +85,6 @@ class MatrixBuildResult: consolidation: Consolidation accounts: list[ChartOfAccount] selection: MatrixSelection - rows: tuple[ConsolidatedRow, ...] rule: EliminationRule | None @@ -215,17 +213,6 @@ def calculate_matrix( return IntercompanyMatrixOut(companies=list(company_info), cells=cells) -def get_unpaired_rows(rows: Iterable[ConsolidatedRow], matrix: IntercompanyMatrixOut) -> list[ConsolidatedRow]: - """Строки свода, попавшие только на одну сторону пары шахматки §6.""" - entry_ids = { - entry_id - for cell in matrix.cells - if cell.one_sided - for entry_id in (*cell.osv_entry_ids_debit, *cell.osv_entry_ids_credit) - } - return [row for row in rows if entry_ids.intersection(row.entry_ids)] - - def _make_account_tree( accounts: Sequence[ChartOfAccount], ) -> tuple[dict[uuid.UUID, ChartOfAccount], dict[uuid.UUID, list[uuid.UUID]]]: @@ -429,14 +416,6 @@ class IntercompanyMatrixService: ) return result.matrix, info - async def get_unpaired_osv(self, version_id: uuid.UUID, rule_id: uuid.UUID) -> UnpairedOsvOut: - """Строки свода без встречной стороны в шахматке §6 заданного пресета.""" - result = await self._build_accounts( - version_id, mode="PRESET", debit_account_ids=(), credit_account_ids=(), rule_id=rule_id - ) - unmatched = get_unpaired_rows(result.rows, result.matrix) - return UnpairedOsvOut(items=[row_out(row) for row in unmatched], total=len(unmatched)) - async def _resolve_preset_selection( self, rule_id: uuid.UUID | None, @@ -517,6 +496,5 @@ class IntercompanyMatrixService: consolidation=osv_result.consolidation, 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 22e15b8..35ced23 100644 --- a/api/tests/test_api_intercompany_matrix.py +++ b/api/tests/test_api_intercompany_matrix.py @@ -202,33 +202,6 @@ async def test_preset_rejects_income_rule(auth_client: AuthClientFactory, seeded 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 = ( - 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() - debit_document_id = await _source(seeded, world, credit=False, amount="0", account="62.01") - debit_entry = (await seeded.scalars(select(OsvEntry).where(OsvEntry.document_id == debit_document_id))).one() - - client = await auth_client(KA_RF) - url = f"/consolidations/versions/{world.version.id}/intercompany-matrix/preset/{rule.id}/unpaired-osv" - response = await client.get(url) - assert response.status_code == OK, response.text - assert response.json()["total"] == 1 - assert set(response.json()) == {"items", "total"} - assert response.json()["items"][0]["entry_ids"] == [str(debit_entry.id)] - assert response.json()["items"][0]["closing_debit"] == "0.00" - - await _source(seeded, world, credit=True, amount="100", account="60.01") - paired = await client.get(url) - assert paired.status_code == OK, paired.text - assert paired.json()["items"] == [] - assert paired.json()["total"] == 0 - assert (await client.get(url.replace(str(rule.id), str(uuid.uuid4())))).status_code == NOT_FOUND - - async def test_preset_uses_current_rules_when_version_unfixed( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: diff --git a/api/tests/test_intercompany_matrix.py b/api/tests/test_intercompany_matrix.py index d417d72..7ffa900 100644 --- a/api/tests/test_intercompany_matrix.py +++ b/api/tests/test_intercompany_matrix.py @@ -49,10 +49,7 @@ def test_routes_are_separate_and_matrix_expands_manual_accounts() -> None: assert ("/consolidations/versions/{version_id}/intercompany-matrix/all", ("GET",)) in paths assert ("/consolidations/versions/{version_id}/intercompany-matrix/manual", ("POST",)) in paths assert ("/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}", ("GET",)) in paths - assert ( - "/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}/unpaired-osv", - ("GET",), - ) in paths + assert not any(path.endswith("/unpaired-osv") for path, _ in paths) assert ("/consolidations/versions/{version_id}/intercompany-matrix/result-table", ("GET",)) in paths debit_parent = _account(AccountKind.A) @@ -92,26 +89,6 @@ def test_routes_are_separate_and_matrix_expands_manual_accounts() -> None: ] -def test_unpaired_rows_uses_source_presence_and_preserves_osv_order() -> None: - company_a, company_b = uuid.uuid4(), uuid.uuid4() - debit_account, credit_account = uuid.uuid4(), uuid.uuid4() - paired_debit = _row(company_a, company_b, debit_account, debit="100") - paired_credit = _row(company_b, company_a, credit_account, credit="100") - unpaired_credit = _row(company_a, company_b, credit_account, credit="0") - for row in (paired_debit, paired_credit, unpaired_credit): - row.entry_ids = (uuid.uuid4(),) - rows = [unpaired_credit, paired_debit, paired_credit] - companies = [ - IntercompanyMatrixCompanyOut(id=company_a, name="А"), - IntercompanyMatrixCompanyOut(id=company_b, name="Б"), - ] - result = matrix.calculate_matrix( - rows, companies, matrix.MatrixSelection(frozenset({debit_account}), frozenset({credit_account}), "closing") - ) - - assert matrix.get_unpaired_rows(rows, result) == [unpaired_credit] - - async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkeypatch: pytest.MonkeyPatch) -> None: parent = _account(AccountKind.AP) child = _account(AccountKind.AP, parent.id) diff --git a/api/tests/test_openapi_routes.py b/api/tests/test_openapi_routes.py index 2e397df..6a46dd8 100644 --- a/api/tests/test_openapi_routes.py +++ b/api/tests/test_openapi_routes.py @@ -80,7 +80,6 @@ EXPECTED_ROUTES: dict[str, set[str]] = { "/api/v1/consolidations/versions/{version_id}/intercompany-matrix/manual": {"post"}, "/api/v1/consolidations/versions/{version_id}/intercompany-matrix/manual/export": {"post"}, "/api/v1/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}": {"get"}, - "/api/v1/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}/unpaired-osv": {"get"}, "/api/v1/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}/export": {"get"}, "/api/v1/consolidations/versions/{version_id}/intercompany-matrix/result-table": {"get"}, } @@ -181,11 +180,6 @@ def test_consolidation_routes_declare_documented_responses(schema: dict[str, Any summary = schema["paths"]["/api/v1/consolidations/versions/{version_id}/osv/summary"]["get"]["responses"] assert {"200", "404"} <= set(summary) assert summary["200"]["content"]["application/json"]["schema"]["$ref"].endswith("/OsvSummaryOut") - unpaired = schema["paths"][ - "/api/v1/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}/unpaired-osv" - ]["get"] - assert {parameter["name"] for parameter in unpaired["parameters"]} == {"version_id", "rule_id"} - assert unpaired["responses"]["200"]["content"]["application/json"]["schema"]["$ref"].endswith("/UnpairedOsvOut") def test_osv_export_route_declares_binary_xlsx_response(schema: dict[str, Any]) -> None: diff --git a/openspec/changes/intercompany-matrix-presets/design.md b/openspec/changes/intercompany-matrix-presets/design.md index 4ef47b2..9ae8db4 100644 --- a/openspec/changes/intercompany-matrix-presets/design.md +++ b/openspec/changes/intercompany-matrix-presets/design.md @@ -21,8 +21,7 @@ change не входит. Шахматка — чистая функция от `amount_a` ячейки `(A, B)` на `debit_adjustment` корректировки (`legal_entity_id = A`, `counterparty_id = B`) и `amount_b` на `credit_adjustment` корректировки `(B, A)`; расхождение, процент и порог пересчитываются, ID корректировок попадают в `correction_ids_debit/credit`. -5. `unpaired-osv` строится из `one_sided`-ячеек шахматки пресета (`osv_entry_ids_*`) и возвращает строки сводной ОСВ, - содержащие эти записи. +5. Первоначальная ручка `unpaired-osv` отменена решением владельца ниже и в текущую реализацию не входит. 6. `GET …/elimination-rules` отдаёт объект с основанием выбора версии; `as_of` не вводится. 7. xlsx пресета: имя `intercompany-matrix_<ГК>_<год>-<квартал>_v_rule-balance-.xlsx`, в шапке «Режим = Пресет», номер правила, статьи сторон, фактическая версия правил, коды счетов сторон (отсортированы) и строка «Показатель» diff --git a/openspec/changes/intercompany-matrix-presets/proposal.md b/openspec/changes/intercompany-matrix-presets/proposal.md index 6b4f00d..2a82c61 100644 --- a/openspec/changes/intercompany-matrix-presets/proposal.md +++ b/openspec/changes/intercompany-matrix-presets/proposal.md @@ -1,15 +1,14 @@ ## Why Шахматка ВГО (ТЗ §6) считалась только по всем счетам (ALL) или по счетам, выбранным вручную (MANUAL). Аналитику нужен -режим «пресет»: счета и показатель берутся из правила элиминации (Прил. 3) и со -списком строк сводной ОСВ без пары. Реализация — PR RSHB/arm-core#141 (ветка `add-preset-shahmatka`, автор — Потапова). +режим «пресет»: счета и показатель берутся из правила элиминации (Прил. 3). +Реализация — PR RSHB/arm-core#141 (ветка `add-preset-shahmatka`, автор — Потапова). Требования оформлены как дельты этого change'а при ревью (решение владельца 21.10.2026); спорное поведение вынесено в `design.md`, `## Known issues`, и в спеке не закреплено как окончательное. ## What Changes -- Режим PRESET шахматки: `GET …/intercompany-matrix/preset/{rule_id}`, выгрузка `…/preset/{rule_id}/export`, список - строк без пары `…/preset/{rule_id}/unpaired-osv`. +- Режим PRESET шахматки: `GET …/intercompany-matrix/preset/{rule_id}`, выгрузка `…/preset/{rule_id}/export`. - Показатель пресета (`closing` / `turnover`) берётся из правила. - `GET …/elimination-rules` возвращает объект `{version_no, is_version_fixed, rules}` вместо массива; у правила есть `id`. Для версии без зафиксированного `elimination_rules_version` отдаются открытые правила (`valid_to IS NULL`) @@ -24,7 +23,7 @@ ### Modified Capabilities - `consolidations-api`: ответ правил элиминации версии. -- `consolidated-osv`: режим PRESET шахматки, строки без пары, xlsx пресета. +- `consolidated-osv`: режим PRESET шахматки и xlsx пресета. - `audit-trail`: аудит экспорта шахматки (режим PRESET). ## Impact 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 3adf433..c508637 100644 --- a/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md +++ b/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md @@ -32,24 +32,6 @@ - **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}`: все строки сводной ОСВ версии, чьи записи ОСВ -входят в `osv_entry_ids_debit/credit` ячеек шахматки пресета с `one_sided = true`. Формат строки — как на странице -сводной ОСВ, без пагинации; `total` — число элементов. Ошибки — как у расчёта пресета. - -#### Scenario: Есть односторонняя ячейка -- **WHEN** в шахматке пресета у ячейки `(A, B)` `one_sided = true` -- **THEN** `items` содержит строки сводной ОСВ с записями этой ячейки, `total` равен их числу - -#### Scenario: Все пары двусторонние -- **WHEN** у шахматки пресета нет односторонних ячеек -- **THEN** `items = []`, `total = 0` - -#### Scenario: Неизвестное правило -- **WHEN** `rule_id` не существует -- **THEN** 404 - ### Requirement: XLSX шахматки пресета `GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v_rule-balance-.xlsx`. Шапка SHALL diff --git a/openspec/changes/intercompany-matrix-presets/tasks.md b/openspec/changes/intercompany-matrix-presets/tasks.md index 6db50ca..ea5bb8d 100644 --- a/openspec/changes/intercompany-matrix-presets/tasks.md +++ b/openspec/changes/intercompany-matrix-presets/tasks.md @@ -7,8 +7,8 @@ - [x] 2.1 Отдельным коммитом убрать учёт корректировок (`_apply_corrections`, `correction_for_version_consolidation`, `correction_ids_*`) вместе с тестами; код выделен в `AURORA-1747`. -- [ ] 2.1a Убрать ручку `unpaired-osv` (`get_unpaired_rows`, `UnpairedOsvOut`, маршрут) вместе с тестами; +- [x] 2.1a Убрать ручку `unpaired-osv` (`get_unpaired_rows`, `UnpairedOsvOut`, маршрут) вместе с тестами; код сохранён в `wip/shahmatka-corrections-unpaired`. - [x] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест. - [x] 2.3 Несколько открытых `version_no` — только максимальная версия; вернуть её номер, тест. -- [ ] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README. +- [x] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README.