fix(audit): сохранять номер и версию правила в экспорте пресета
This commit is contained in:
parent
3eb5362e58
commit
b078933428
@ -876,7 +876,8 @@ API: список счетов `GET /directories/accounts`, правила ве
|
|||||||
`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 — строка «Показатель»,
|
||||||
audit `EXPORT` пишет `mode` (`ALL`/`MANUAL`/`PRESET`) и `rule_id`. Открытые вопросы по пресетам —
|
audit `EXPORT` пишет `mode` (`ALL`/`MANUAL`/`PRESET`), для PRESET — `rule_id`, `rule_no` и фактический
|
||||||
|
`rule_version_no`. Открытые вопросы по пресетам —
|
||||||
`openspec/changes/intercompany-matrix-presets/design.md`. Контракт для фронта —
|
`openspec/changes/intercompany-matrix-presets/design.md`. Контракт для фронта —
|
||||||
`../docs/dev/frontend-api-guide.md` §5.8; требования — в существующих спецификациях
|
`../docs/dev/frontend-api-guide.md` §5.8; требования — в существующих спецификациях
|
||||||
`openspec/specs/{directories-api,consolidations-api,consolidated-osv,audit-trail}/spec.md`.
|
`openspec/specs/{directories-api,consolidations-api,consolidated-osv,audit-trail}/spec.md`.
|
||||||
|
|||||||
@ -52,6 +52,8 @@ async def _export_response(
|
|||||||
*,
|
*,
|
||||||
selection: IntercompanyMatrixManualIn | None = None,
|
selection: IntercompanyMatrixManualIn | None = None,
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
if info.mode == "PRESET" and (info.rule_id is None or info.rule_no is None or info.rule_version_no is None):
|
||||||
|
raise ValueError("Для аудита PRESET нужны ID, номер и версия правила")
|
||||||
response = await _xlsx_response(matrix, info)
|
response = await _xlsx_response(matrix, info)
|
||||||
await audit.record(
|
await audit.record(
|
||||||
action=AuditAction.EXPORT,
|
action=AuditAction.EXPORT,
|
||||||
@ -64,7 +66,15 @@ async def _export_response(
|
|||||||
"mode": info.mode,
|
"mode": info.mode,
|
||||||
"debit_account_ids": [str(account_id) for account_id in selection.debit_account_ids] if selection else [],
|
"debit_account_ids": [str(account_id) for account_id in selection.debit_account_ids] if selection else [],
|
||||||
"credit_account_ids": [str(account_id) for account_id in selection.credit_account_ids] if selection else [],
|
"credit_account_ids": [str(account_id) for account_id in selection.credit_account_ids] if selection else [],
|
||||||
**({"rule_id": str(info.rule_id)} if info.rule_id is not None else {}),
|
**(
|
||||||
|
{
|
||||||
|
"rule_id": str(info.rule_id),
|
||||||
|
"rule_no": info.rule_no,
|
||||||
|
"rule_version_no": info.rule_version_no,
|
||||||
|
}
|
||||||
|
if info.mode == "PRESET"
|
||||||
|
else {}
|
||||||
|
),
|
||||||
"cells": len(matrix.cells),
|
"cells": len(matrix.cells),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -87,6 +87,7 @@ class MatrixBuildResult:
|
|||||||
accounts: list[ChartOfAccount]
|
accounts: list[ChartOfAccount]
|
||||||
selection: MatrixSelection
|
selection: MatrixSelection
|
||||||
rows: tuple[ConsolidatedRow, ...]
|
rows: tuple[ConsolidatedRow, ...]
|
||||||
|
rule: EliminationRule | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -100,6 +101,8 @@ class MatrixExportInfo:
|
|||||||
credit_account_codes: tuple[str, ...]
|
credit_account_codes: tuple[str, ...]
|
||||||
measure: Measure = "closing"
|
measure: Measure = "closing"
|
||||||
rule_id: uuid.UUID | None = None
|
rule_id: uuid.UUID | None = None
|
||||||
|
rule_no: int | None = None
|
||||||
|
rule_version_no: int | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self) -> str:
|
def filename(self) -> str:
|
||||||
@ -394,7 +397,9 @@ class IntercompanyMatrixService:
|
|||||||
debit_account_codes=debit_codes,
|
debit_account_codes=debit_codes,
|
||||||
credit_account_codes=credit_codes,
|
credit_account_codes=credit_codes,
|
||||||
measure=result.selection.measure,
|
measure=result.selection.measure,
|
||||||
rule_id=rule_id,
|
rule_id=result.rule.id if result.rule is not None else None,
|
||||||
|
rule_no=result.rule.rule_no if result.rule is not None else None,
|
||||||
|
rule_version_no=result.rule.version_no if result.rule is not None else None,
|
||||||
)
|
)
|
||||||
return result.matrix, info
|
return result.matrix, info
|
||||||
|
|
||||||
@ -462,6 +467,7 @@ class IntercompanyMatrixService:
|
|||||||
rows = osv_result.consolidated.rows
|
rows = osv_result.consolidated.rows
|
||||||
accounts = await directory_repo.list_accounts(self._session)
|
accounts = await directory_repo.list_accounts(self._session)
|
||||||
by_id, children = _make_account_tree(accounts)
|
by_id, children = _make_account_tree(accounts)
|
||||||
|
rule: EliminationRule | None = None
|
||||||
if mode == "ALL":
|
if mode == "ALL":
|
||||||
if debit_account_ids or credit_account_ids:
|
if debit_account_ids or credit_account_ids:
|
||||||
raise DomainValidationError("ACCOUNTS_NOT_ALLOWED", "Для режима ALL не нужно указывать счета")
|
raise DomainValidationError("ACCOUNTS_NOT_ALLOWED", "Для режима ALL не нужно указывать счета")
|
||||||
@ -475,7 +481,7 @@ class IntercompanyMatrixService:
|
|||||||
credit = _select_accounts(credit_account_ids, (), "K", by_id, children)
|
credit = _select_accounts(credit_account_ids, (), "K", by_id, children)
|
||||||
selection = MatrixSelection(debit, credit, "closing")
|
selection = MatrixSelection(debit, credit, "closing")
|
||||||
elif mode == "PRESET":
|
elif mode == "PRESET":
|
||||||
selection, _ = await self._resolve_preset_selection(rule_id, osv_result.version, by_id, children)
|
selection, rule = await self._resolve_preset_selection(rule_id, osv_result.version, by_id, children)
|
||||||
else:
|
else:
|
||||||
raise DomainValidationError("INVALID_MODE", "Неизвестный режим выбора счетов")
|
raise DomainValidationError("INVALID_MODE", "Неизвестный режим выбора счетов")
|
||||||
matrix = self._calculate(rows, osv_result.members, selection)
|
matrix = self._calculate(rows, osv_result.members, selection)
|
||||||
@ -486,4 +492,5 @@ class IntercompanyMatrixService:
|
|||||||
accounts=accounts,
|
accounts=accounts,
|
||||||
selection=selection,
|
selection=selection,
|
||||||
rows=rows,
|
rows=rows,
|
||||||
|
rule=rule,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -177,6 +177,8 @@ async def test_preset_uses_rule_sides_and_exclusions(auth_client: AuthClientFact
|
|||||||
audit = (await seeded.scalars(select(AuditEvent).where(AuditEvent.action == AuditAction.EXPORT))).one()
|
audit = (await seeded.scalars(select(AuditEvent).where(AuditEvent.action == AuditAction.EXPORT))).one()
|
||||||
assert audit.after_value["mode"] == "PRESET"
|
assert audit.after_value["mode"] == "PRESET"
|
||||||
assert audit.after_value["rule_id"] == str(rule.id)
|
assert audit.after_value["rule_id"] == str(rule.id)
|
||||||
|
assert audit.after_value["rule_no"] == rule.rule_no
|
||||||
|
assert audit.after_value["rule_version_no"] == rule.version_no
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("suffix", ["", "/export"])
|
@pytest.mark.parametrize("suffix", ["", "/export"])
|
||||||
@ -241,6 +243,9 @@ async def test_preset_uses_current_rules_when_version_unfixed(
|
|||||||
response = await client.get(url)
|
response = await client.get(url)
|
||||||
assert response.status_code == OK, response.text
|
assert response.status_code == OK, response.text
|
||||||
assert (await client.get(f"{url}/export")).status_code == OK
|
assert (await client.get(f"{url}/export")).status_code == OK
|
||||||
|
audit = (await seeded.scalars(select(AuditEvent).where(AuditEvent.action == AuditAction.EXPORT))).one()
|
||||||
|
assert audit.after_value["rule_no"] == rule.rule_no
|
||||||
|
assert audit.after_value["rule_version_no"] == rule.version_no
|
||||||
assert world.version.elimination_rules_version is None
|
assert world.version.elimination_rules_version is None
|
||||||
world.version.elimination_rules_version = rule.version_no + 1
|
world.version.elimination_rules_version = rule.version_no + 1
|
||||||
await seeded.flush()
|
await seeded.flush()
|
||||||
|
|||||||
@ -129,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]))
|
monkeypatch.setattr(matrix.directory_repo, "list_accounts", AsyncMock(return_value=[parent, child]))
|
||||||
rule = SimpleNamespace(
|
rule = SimpleNamespace(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
|
rule_no=6,
|
||||||
statement_type=StatementType.balance,
|
statement_type=StatementType.balance,
|
||||||
version_no=1,
|
version_no=1,
|
||||||
side_1="D",
|
side_1="D",
|
||||||
@ -165,6 +166,8 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey
|
|||||||
assert info.debit_account_codes == ("76",)
|
assert info.debit_account_codes == ("76",)
|
||||||
assert info.credit_account_codes == ("76", "76.01")
|
assert info.credit_account_codes == ("76", "76.01")
|
||||||
assert info.measure == "turnover"
|
assert info.measure == "turnover"
|
||||||
|
assert info.rule_no == rule.rule_no
|
||||||
|
assert info.rule_version_no == rule.version_no
|
||||||
|
|
||||||
|
|
||||||
async def test_preset_rejects_income_rule_before_selecting_accounts(monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_preset_rejects_income_rule_before_selecting_accounts(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
|||||||
@ -37,6 +37,8 @@ def _info(*, mode: str = "MANUAL", rule_id: UUID | None = None) -> MatrixExportI
|
|||||||
credit_account_codes=("60.01",) if mode != "ALL" else (),
|
credit_account_codes=("60.01",) if mode != "ALL" else (),
|
||||||
measure="turnover" if mode == "PRESET" else "closing",
|
measure="turnover" if mode == "PRESET" else "closing",
|
||||||
rule_id=rule_id,
|
rule_id=rule_id,
|
||||||
|
rule_no=6 if mode == "PRESET" else None,
|
||||||
|
rule_version_no=3 if mode == "PRESET" else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -135,7 +137,6 @@ def test_matrix_export_has_three_rows_per_debit_company_and_highlights_threshold
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def test_export_routes_use_matrix_calculation() -> None:
|
async def test_export_routes_use_matrix_calculation() -> None:
|
||||||
version_id = UUID("11111111-1111-1111-1111-111111111111")
|
version_id = UUID("11111111-1111-1111-1111-111111111111")
|
||||||
account_id = UUID("22222222-2222-2222-2222-222222222222")
|
account_id = UUID("22222222-2222-2222-2222-222222222222")
|
||||||
@ -234,6 +235,8 @@ async def test_export_routes_use_matrix_calculation() -> None:
|
|||||||
"debit_account_ids": [],
|
"debit_account_ids": [],
|
||||||
"credit_account_ids": [],
|
"credit_account_ids": [],
|
||||||
"rule_id": str(account_id),
|
"rule_id": str(account_id),
|
||||||
|
"rule_no": 6,
|
||||||
|
"rule_version_no": 3,
|
||||||
"cells": 1,
|
"cells": 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -51,8 +51,7 @@ change не входит. Шахматка — чистая функция от
|
|||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
1. Имя файла содержит UUID правила, шапка xlsx не содержит номер правила и версию; audit EXPORT не пишет номер и
|
1. Имя файла содержит UUID правила, шапка xlsx не содержит номер правила и версию.
|
||||||
версию правил.
|
|
||||||
|
|
||||||
## Decisions made without discussion
|
## Decisions made without discussion
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
`id`. Для версии без зафиксированного `elimination_rules_version` отдаются открытые правила (`valid_to IS NULL`)
|
`id`. Для версии без зафиксированного `elimination_rules_version` отдаются открытые правила (`valid_to IS NULL`)
|
||||||
с максимальным `version_no`; ответ содержит этот номер.
|
с максимальным `version_no`; ответ содержит этот номер.
|
||||||
- xlsx: для PRESET имя файла с `_preset-<rule_id>`, в шапке строка «Показатель»; audit `EXPORT` пишет `mode`
|
- xlsx: для PRESET имя файла с `_preset-<rule_id>`, в шапке строка «Показатель»; audit `EXPORT` пишет `mode`
|
||||||
`ALL`/`MANUAL`/`PRESET` и `rule_id`.
|
`ALL`/`MANUAL`/`PRESET`, а для PRESET также `rule_id`, `rule_no` и фактический `rule_version_no`.
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
SHALL добавлять отдельное неизменяемое событие `audit_event` с `action = EXPORT`, `section = EXPORT`,
|
SHALL добавлять отдельное неизменяемое событие `audit_event` с `action = EXPORT`, `section = EXPORT`,
|
||||||
`object_type = consolidation_version`, `object_id = consolidation_version_id`, `user_id` инициатора,
|
`object_type = consolidation_version`, `object_id = consolidation_version_id`, `user_id` инициатора,
|
||||||
`before_value = null` и `after_value = {report: "intercompany_matrix", mode, debit_account_ids,
|
`before_value = null` и `after_value = {report: "intercompany_matrix", mode, debit_account_ids,
|
||||||
credit_account_ids, cells}`; для `PRESET` в `after_value` SHALL быть дополнительно `rule_id`. `mode` — `ALL`,
|
credit_account_ids, cells}`; для `PRESET` в `after_value` SHALL быть дополнительно `rule_id`, `rule_no` и
|
||||||
|
`rule_version_no` (фактическая версия применённого правила, в том числе при незафиксированной версии правил). `mode` — `ALL`,
|
||||||
`MANUAL` или `PRESET`, массивы счетов — ID из запроса (в ALL и PRESET пустые), `cells` — число ячеек. Выгрузка
|
`MANUAL` или `PRESET`, массивы счетов — ID из запроса (в ALL и PRESET пустые), `cells` — число ячеек. Выгрузка
|
||||||
сводной ОСВ продолжает писать собственное событие `EXPORT`.
|
сводной ОСВ продолжает писать собственное событие `EXPORT`.
|
||||||
|
|
||||||
@ -16,4 +17,4 @@ credit_account_ids, cells}`; для `PRESET` в `after_value` SHALL быть д
|
|||||||
|
|
||||||
#### Scenario: Выгрузка пресета
|
#### Scenario: Выгрузка пресета
|
||||||
- **WHEN** пользователь выгружает `GET …/intercompany-matrix/preset/{rule_id}/export`
|
- **WHEN** пользователь выгружает `GET …/intercompany-matrix/preset/{rule_id}/export`
|
||||||
- **THEN** событие `EXPORT` содержит `mode = PRESET`, `rule_id` правила, пустые массивы счетов и число ячеек
|
- **THEN** событие `EXPORT` содержит `mode = PRESET`, `rule_id`, `rule_no`, `rule_version_no`, пустые массивы счетов и число ячеек
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user