add-preset-shahmatka #141
@ -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 пресета оканчивается на
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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):
|
||||
"""Пробел свода: у ЮЛ по счёту нет квартальных ОСВ за перечисленные кварталы (строки по нему не строятся)."""
|
||||
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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`, в шапке «Режим = Пресет»,
|
||||
номер правила, статьи сторон, фактическая версия правил, коды счетов сторон (отсортированы) и строка «Показатель»
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user