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`, матрица
`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 пресета оканчивается на

View File

@ -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,

View File

@ -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):
"""Пробел свода: у ЮЛ по счёту нет квартальных ОСВ за перечисленные кварталы (строки по нему не строятся)."""

View File

@ -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,
)

View File

@ -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:

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/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)

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/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:

View File

@ -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<n>_rule-balance-<rule_no>.xlsx`, в шапке «Режим = Пресет»,
номер правила, статьи сторон, фактическая версия правил, коды счетов сторон (отсортированы) и строка «Показатель»

View File

@ -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

View File

@ -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<n>_rule-balance-<rule_no>.xlsx`. Шапка SHALL

View File

@ -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.