refactor(matrix): удалить unpaired-osv из ветки пресетов

This commit is contained in:
PotapovaA 2026-10-21 19:32:23 +03:00
parent fabc9b48b2
commit 3de0c90198
11 changed files with 10 additions and 133 deletions

View File

@ -871,8 +871,7 @@ Q2–Q4 не подходят (R-14a). Привязываются только `
API: список счетов `GET /directories/accounts`, правила версии `GET …/elimination-rules`, матрица API: список счетов `GET /directories/accounts`, правила версии `GET …/elimination-rules`, матрица
`GET …/intercompany-matrix/all` и `POST …/manual`, их XLSX-выгрузки, статичная таблица `GET …/intercompany-matrix/all` и `POST …/manual`, их XLSX-выгрузки, статичная таблица
`GET …/intercompany-matrix/result-table`. Режим PRESET (счета и показатель из правила элиминации): `GET …/intercompany-matrix/result-table`. Режим PRESET (счета и показатель из правила элиминации):
`GET …/intercompany-matrix/preset/{rule_id}`, его выгрузка `…/preset/{rule_id}/export` и строки сводной ОСВ без пары `GET …/intercompany-matrix/preset/{rule_id}` и его выгрузка `…/preset/{rule_id}/export`.
`…/preset/{rule_id}/unpaired-osv`.
`GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной `GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной
версии правил — открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ сообщает этот номер. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`, версии правил — открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ сообщает этот номер. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`,
`ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. Имя XLSX пресета оканчивается на `ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. Имя XLSX пресета оканчивается на

View File

@ -16,7 +16,6 @@ from src.schemas.consolidations import (
IntercompanyMatrixManualIn, IntercompanyMatrixManualIn,
IntercompanyMatrixOut, IntercompanyMatrixOut,
IntercompanyResultTableOut, IntercompanyResultTableOut,
UnpairedOsvOut,
) )
from src.services.intercompany_matrix import MatrixExportInfo from src.services.intercompany_matrix import MatrixExportInfo
from src.services.intercompany_matrix_export import build_matrix_workbook 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) 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( @router.get(
"/{version_id}/intercompany-matrix/preset/{rule_id}/export", "/{version_id}/intercompany-matrix/preset/{rule_id}/export",
response_class=Response, response_class=Response,

View File

@ -232,13 +232,6 @@ class ConsolidatedRowOut(BaseModel):
return None if value is None else value.quantize(CENT) return None if value is None else value.quantize(CENT)
class UnpairedOsvOut(BaseModel):
"""Все строки сводной ОСВ без встречной стороны по правилу и их количество."""
items: list[ConsolidatedRowOut]
total: int
class GapOut(BaseModel): class GapOut(BaseModel):
"""Пробел свода: у ЮЛ по счёту нет квартальных ОСВ за перечисленные кварталы (строки по нему не строятся).""" """Пробел свода: у ЮЛ по счёту нет квартальных ОСВ за перечисленные кварталы (строки по нему не строятся)."""

View File

@ -26,9 +26,8 @@ from src.schemas.consolidations import (
IntercompanyResultAmountOut, IntercompanyResultAmountOut,
IntercompanyResultArticleOut, IntercompanyResultArticleOut,
IntercompanyResultTableOut, IntercompanyResultTableOut,
UnpairedOsvOut,
) )
from src.services.consolidated_osv import ConsolidatedOsvService, row_out from src.services.consolidated_osv import ConsolidatedOsvService
Measure = Literal["closing", "turnover"] Measure = Literal["closing", "turnover"]
Side = Literal["D", "K"] Side = Literal["D", "K"]
@ -86,7 +85,6 @@ class MatrixBuildResult:
consolidation: Consolidation consolidation: Consolidation
accounts: list[ChartOfAccount] accounts: list[ChartOfAccount]
selection: MatrixSelection selection: MatrixSelection
rows: tuple[ConsolidatedRow, ...]
rule: EliminationRule | None rule: EliminationRule | None
@ -215,17 +213,6 @@ def calculate_matrix(
return IntercompanyMatrixOut(companies=list(company_info), cells=cells) 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( def _make_account_tree(
accounts: Sequence[ChartOfAccount], accounts: Sequence[ChartOfAccount],
) -> tuple[dict[uuid.UUID, ChartOfAccount], dict[uuid.UUID, list[uuid.UUID]]]: ) -> tuple[dict[uuid.UUID, ChartOfAccount], dict[uuid.UUID, list[uuid.UUID]]]:
@ -429,14 +416,6 @@ class IntercompanyMatrixService:
) )
return result.matrix, info 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( async def _resolve_preset_selection(
self, self,
rule_id: uuid.UUID | None, rule_id: uuid.UUID | None,
@ -517,6 +496,5 @@ class IntercompanyMatrixService:
consolidation=osv_result.consolidation, consolidation=osv_result.consolidation,
accounts=accounts, accounts=accounts,
selection=selection, selection=selection,
rows=rows,
rule=rule, rule=rule,
) )

View File

@ -202,33 +202,6 @@ async def test_preset_rejects_income_rule(auth_client: AuthClientFactory, seeded
assert response.json()["detail"]["code"] == "INVALID_RULE" 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( async def test_preset_uses_current_rules_when_version_unfixed(
auth_client: AuthClientFactory, seeded: AsyncSession auth_client: AuthClientFactory, seeded: AsyncSession
) -> None: ) -> None:

View File

@ -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/all", ("GET",)) in paths
assert ("/consolidations/versions/{version_id}/intercompany-matrix/manual", ("POST",)) 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}", ("GET",)) in paths
assert ( assert not any(path.endswith("/unpaired-osv") for path, _ in paths)
"/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}/unpaired-osv",
("GET",),
) in paths
assert ("/consolidations/versions/{version_id}/intercompany-matrix/result-table", ("GET",)) in paths assert ("/consolidations/versions/{version_id}/intercompany-matrix/result-table", ("GET",)) in paths
debit_parent = _account(AccountKind.A) 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: async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkeypatch: pytest.MonkeyPatch) -> None:
parent = _account(AccountKind.AP) parent = _account(AccountKind.AP)
child = _account(AccountKind.AP, parent.id) child = _account(AccountKind.AP, parent.id)

View File

@ -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": {"post"},
"/api/v1/consolidations/versions/{version_id}/intercompany-matrix/manual/export": {"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}": {"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/preset/{rule_id}/export": {"get"},
"/api/v1/consolidations/versions/{version_id}/intercompany-matrix/result-table": {"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"] summary = schema["paths"]["/api/v1/consolidations/versions/{version_id}/osv/summary"]["get"]["responses"]
assert {"200", "404"} <= set(summary) assert {"200", "404"} <= set(summary)
assert summary["200"]["content"]["application/json"]["schema"]["$ref"].endswith("/OsvSummaryOut") 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: def test_osv_export_route_declares_binary_xlsx_response(schema: dict[str, Any]) -> None:

View File

@ -21,8 +21,7 @@ change не входит. Шахматка — чистая функция от
`amount_a` ячейки `(A, B)` на `debit_adjustment` корректировки (`legal_entity_id = A`, `counterparty_id = B`) и `amount_a` ячейки `(A, B)` на `debit_adjustment` корректировки (`legal_entity_id = A`, `counterparty_id = B`) и
`amount_b` на `credit_adjustment` корректировки `(B, A)`; расхождение, процент и порог пересчитываются, ID `amount_b` на `credit_adjustment` корректировки `(B, A)`; расхождение, процент и порог пересчитываются, ID
корректировок попадают в `correction_ids_debit/credit`. корректировок попадают в `correction_ids_debit/credit`.
5. `unpaired-osv` строится из `one_sided`-ячеек шахматки пресета (`osv_entry_ids_*`) и возвращает строки сводной ОСВ, 5. Первоначальная ручка `unpaired-osv` отменена решением владельца ниже и в текущую реализацию не входит.
содержащие эти записи.
6. `GET …/elimination-rules` отдаёт объект с основанием выбора версии; `as_of` не вводится. 6. `GET …/elimination-rules` отдаёт объект с основанием выбора версии; `as_of` не вводится.
7. xlsx пресета: имя `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.xlsx`, в шапке «Режим = Пресет», 7. xlsx пресета: имя `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.xlsx`, в шапке «Режим = Пресет»,
номер правила, статьи сторон, фактическая версия правил, коды счетов сторон (отсортированы) и строка «Показатель» номер правила, статьи сторон, фактическая версия правил, коды счетов сторон (отсортированы) и строка «Показатель»

View File

@ -1,15 +1,14 @@
## Why ## Why
Шахматка ВГО (ТЗ §6) считалась только по всем счетам (ALL) или по счетам, выбранным вручную (MANUAL). Аналитику нужен Шахматка ВГО (ТЗ §6) считалась только по всем счетам (ALL) или по счетам, выбранным вручную (MANUAL). Аналитику нужен
режим «пресет»: счета и показатель берутся из правила элиминации (Прил. 3) и со режим «пресет»: счета и показатель берутся из правила элиминации (Прил. 3).
списком строк сводной ОСВ без пары. Реализация — PR RSHB/arm-core#141 (ветка `add-preset-shahmatka`, автор — Потапова). Реализация — PR RSHB/arm-core#141 (ветка `add-preset-shahmatka`, автор — Потапова).
Требования оформлены как дельты этого change'а при ревью (решение владельца 21.10.2026); спорное поведение вынесено в Требования оформлены как дельты этого change'а при ревью (решение владельца 21.10.2026); спорное поведение вынесено в
`design.md`, `## Known issues`, и в спеке не закреплено как окончательное. `design.md`, `## Known issues`, и в спеке не закреплено как окончательное.
## What Changes ## What Changes
- Режим PRESET шахматки: `GET …/intercompany-matrix/preset/{rule_id}`, выгрузка `…/preset/{rule_id}/export`, список - Режим PRESET шахматки: `GET …/intercompany-matrix/preset/{rule_id}`, выгрузка `…/preset/{rule_id}/export`.
строк без пары `…/preset/{rule_id}/unpaired-osv`.
- Показатель пресета (`closing` / `turnover`) берётся из правила. - Показатель пресета (`closing` / `turnover`) берётся из правила.
- `GET …/elimination-rules` возвращает объект `{version_no, is_version_fixed, rules}` вместо массива; у правила есть - `GET …/elimination-rules` возвращает объект `{version_no, is_version_fixed, rules}` вместо массива; у правила есть
`id`. Для версии без зафиксированного `elimination_rules_version` отдаются открытые правила (`valid_to IS NULL`) `id`. Для версии без зафиксированного `elimination_rules_version` отдаются открытые правила (`valid_to IS NULL`)
@ -24,7 +23,7 @@
### Modified Capabilities ### Modified Capabilities
- `consolidations-api`: ответ правил элиминации версии. - `consolidations-api`: ответ правил элиминации версии.
- `consolidated-osv`: режим PRESET шахматки, строки без пары, xlsx пресета. - `consolidated-osv`: режим PRESET шахматки и xlsx пресета.
- `audit-trail`: аудит экспорта шахматки (режим PRESET). - `audit-trail`: аудит экспорта шахматки (режим PRESET).
## Impact ## Impact

View File

@ -32,24 +32,6 @@
- **WHEN** у правила `statement_type = income` - **WHEN** у правила `statement_type = income`
- **THEN** расчёт и выгрузка возвращают 422 `INVALID_RULE` - **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 шахматки пресета ### Requirement: XLSX шахматки пресета
`GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт `GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт
пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.xlsx`. Шапка SHALL пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.xlsx`. Шапка SHALL

View File

@ -7,8 +7,8 @@
- [x] 2.1 Отдельным коммитом убрать учёт корректировок (`_apply_corrections`, `correction_for_version_consolidation`, - [x] 2.1 Отдельным коммитом убрать учёт корректировок (`_apply_corrections`, `correction_for_version_consolidation`,
`correction_ids_*`) вместе с тестами; код выделен в `AURORA-1747`. `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`. код сохранён в `wip/shahmatka-corrections-unpaired`.
- [x] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест. - [x] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест.
- [x] 2.3 Несколько открытых `version_no` — только максимальная версия; вернуть её номер, тест. - [x] 2.3 Несколько открытых `version_no` — только максимальная версия; вернуть её номер, тест.
- [ ] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README. - [x] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README.