refactor(matrix): вынести применение корректировок из ветки пресетов

This commit is contained in:
PotapovaA 2026-10-21 18:40:05 +03:00
parent 6213de4498
commit e9dc67b2e9
12 changed files with 10 additions and 234 deletions

View File

@ -872,7 +872,7 @@ API: список счетов `GET /directories/accounts`, правила ве
`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`; в ячейке `correction_ids_debit/credit` (применённые корректировки правила). `…/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`). Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`, версии правил — открытые правила (`valid_to IS NULL`). Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`,
`ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. В шапке xlsx — строка «Показатель», `ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. В шапке xlsx — строка «Показатель»,

View File

@ -31,7 +31,6 @@ from src.models.directories import CompanyGroup, LegalEntity, LegalEntityAlias,
from src.models.enums import ( from src.models.enums import (
ConsolidationApproach, ConsolidationApproach,
ConsolidationStatus, ConsolidationStatus,
CorrectionStatus,
DocumentStatus, DocumentStatus,
DocumentType, DocumentType,
FileStatus, FileStatus,
@ -41,7 +40,6 @@ from src.models.enums import (
RelatedGroupKind, RelatedGroupKind,
) )
from src.models.files import Document, StoredFile from src.models.files import Document, StoredFile
from src.models.kfo import Correction
from src.models.osv import OsvEntry from src.models.osv import OsvEntry
from src.repositories.directories import LIKE_ESCAPE, escape_like from src.repositories.directories import LIKE_ESCAPE, escape_like
@ -565,22 +563,3 @@ async def current_related_groups(
for entity_id, group in await session.execute(stmt): for entity_id, group in await session.execute(stmt):
groups[entity_id][group.kind] = group groups[entity_id][group.kind] = group
return dict(groups) return dict(groups)
async def correction_for_version_consolidation(
session: AsyncSession, version_id: uuid.UUID, rule_id: uuid.UUID
) -> list[Correction]:
"""Применённые корректировки правила для версии консолидации."""
if not version_id or not rule_id:
return []
stmt = (
select(Correction)
.where(
Correction.consolidation_version_id == version_id,
Correction.rule_id == rule_id,
Correction.status == CorrectionStatus.APPLIED,
)
.order_by(Correction.id)
)
return list((await session.scalars(stmt)).all())

View File

@ -367,8 +367,6 @@ class IntercompanyMatrixCellOut(BaseModel):
) )
osv_entry_ids_debit: list[uuid.UUID] osv_entry_ids_debit: list[uuid.UUID]
osv_entry_ids_credit: list[uuid.UUID] osv_entry_ids_credit: list[uuid.UUID]
correction_ids_debit: list[uuid.UUID] = Field(default_factory=list)
correction_ids_credit: list[uuid.UUID] = Field(default_factory=list)
@field_validator("amount_a", "amount_b", "variance_amount", "variance_percent") @field_validator("amount_a", "amount_b", "variance_amount", "variance_percent")
@classmethod @classmethod

View File

@ -17,7 +17,6 @@ from src.models.accounting import ChartOfAccount, EliminationRule
from src.models.consolidation import Consolidation, ConsolidationVersion from src.models.consolidation import Consolidation, ConsolidationVersion
from src.models.directories import CompanyGroup from src.models.directories import CompanyGroup
from src.models.enums import AccountKind, PeriodQuarter from src.models.enums import AccountKind, PeriodQuarter
from src.repositories import consolidations as consolidation_repo
from src.repositories import directories as directory_repo from src.repositories import directories as directory_repo
from src.repositories import elimination_rules as elimination_rules_repo from src.repositories import elimination_rules as elimination_rules_repo
from src.schemas.consolidations import ( from src.schemas.consolidations import (
@ -407,63 +406,6 @@ class IntercompanyMatrixService:
unmatched = get_unpaired_rows(result.rows, result.matrix) unmatched = get_unpaired_rows(result.rows, result.matrix)
return UnpairedOsvOut(items=[row_out(row) for row in unmatched], total=len(unmatched)) return UnpairedOsvOut(items=[row_out(row) for row in unmatched], total=len(unmatched))
async def _apply_corrections(
self, matrix: IntercompanyMatrixOut, version_id: uuid.UUID, rule_id: uuid.UUID
) -> IntercompanyMatrixOut:
"""Исключение суммы применённых корректировок из сторон пары пресета."""
corrections = await consolidation_repo.correction_for_version_consolidation(
self._session, version_id=version_id, rule_id=rule_id
)
if not corrections:
return matrix
debit_by_pair: dict[tuple[uuid.UUID, uuid.UUID], Decimal] = defaultdict(lambda: ZERO)
credit_by_pair: dict[tuple[uuid.UUID, uuid.UUID], Decimal] = defaultdict(lambda: ZERO)
debit_ids_by_pair: dict[tuple[uuid.UUID, uuid.UUID], set[uuid.UUID]] = defaultdict(set)
credit_ids_by_pair: dict[tuple[uuid.UUID, uuid.UUID], set[uuid.UUID]] = defaultdict(set)
for correction in corrections:
owner, counterparty = correction.legal_entity_id, correction.counterparty_id
if owner is None or counterparty is None:
continue
pair = (owner, counterparty)
if correction.debit_adjustment is not None:
debit_by_pair[pair] += correction.debit_adjustment
debit_ids_by_pair[pair].add(correction.id)
if correction.credit_adjustment is not None:
credit_by_pair[pair] += correction.credit_adjustment
credit_ids_by_pair[pair].add(correction.id)
cells = []
for cell in matrix.cells:
debit_pair = (cell.company_a_id, cell.company_b_id)
credit_pair = (cell.company_b_id, cell.company_a_id)
amount_a = cell.amount_a - debit_by_pair.get(debit_pair, ZERO)
amount_b = cell.amount_b - credit_by_pair.get(credit_pair, ZERO)
debit_ids = list(debit_ids_by_pair.get(debit_pair, ()))
credit_ids = list(credit_ids_by_pair.get(credit_pair, ()))
if not debit_ids and not credit_ids:
cells.append(cell)
continue
variance_amount = abs(amount_a - amount_b)
base = min(amount_a, amount_b)
variance_percent = None if base == ZERO else variance_amount / base * 100
cells.append(
IntercompanyMatrixCellOut.model_validate(
{
**cell.model_dump(),
"amount_a": amount_a,
"amount_b": amount_b,
"correction_ids_debit": debit_ids,
"correction_ids_credit": credit_ids,
"variance_amount": variance_amount,
"variance_percent": variance_percent,
"exceeds_threshold": variance_percent is not None
and variance_percent > VARIANCE_THRESHOLD_PERCENT,
}
)
)
return IntercompanyMatrixOut(companies=matrix.companies, cells=cells)
async def _resolve_preset_selection( async def _resolve_preset_selection(
self, self,
rule_id: uuid.UUID | None, rule_id: uuid.UUID | None,
@ -514,7 +456,6 @@ 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 не нужно указывать счета")
@ -528,12 +469,10 @@ 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, rule = await self._resolve_preset_selection(rule_id, osv_result.version, by_id, children) selection, _ = 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)
if rule is not None:
matrix = await self._apply_corrections(matrix, version_id, rule.id)
return MatrixBuildResult( return MatrixBuildResult(
matrix=matrix, matrix=matrix,
version=osv_result.version, version=osv_result.version,

View File

@ -133,8 +133,6 @@ async def test_no_osv_for_selected_accounts(auth_client: AuthClientFactory, seed
assert { assert {
"osv_entry_ids_debit", "osv_entry_ids_debit",
"osv_entry_ids_credit", "osv_entry_ids_credit",
"correction_ids_debit",
"correction_ids_credit",
"exceeds_threshold", "exceeds_threshold",
} <= cell.keys() } <= cell.keys()
assert {"entry_osv_ids_debit", "entry_osv_ids_credit", "is_more_5_percent"}.isdisjoint(cell) assert {"entry_osv_ids_debit", "entry_osv_ids_credit", "is_more_5_percent"}.isdisjoint(cell)
@ -142,7 +140,6 @@ async def test_no_osv_for_selected_accounts(auth_client: AuthClientFactory, seed
assert cell["variance_percent"] is None assert cell["variance_percent"] is None
assert cell["one_sided"] is False assert cell["one_sided"] is False
assert cell["osv_entry_ids_debit"] == cell["osv_entry_ids_credit"] == [] assert cell["osv_entry_ids_debit"] == cell["osv_entry_ids_credit"] == []
assert cell["correction_ids_debit"] == cell["correction_ids_credit"] == []
assert cell["skipped_osv_entry_count"] == 0 assert cell["skipped_osv_entry_count"] == 0

View File

@ -1,34 +0,0 @@
"""Запрос корректировок к строкам ОСВ версии консолидации."""
import uuid
from types import SimpleNamespace
from unittest.mock import AsyncMock
from sqlalchemy.dialects import postgresql
from src.repositories.consolidations import correction_for_version_consolidation
async def test_correction_query_returns_all_applied_records_without_matching_osv() -> None:
session = AsyncMock()
corrections = [object(), object()]
session.scalars.return_value = SimpleNamespace(all=lambda: corrections)
version_id, rule_id = uuid.uuid4(), uuid.uuid4()
assert await correction_for_version_consolidation(session, version_id, rule_id) == corrections
stmt = session.scalars.await_args.args[0]
sql = str(stmt.compile(dialect=postgresql.dialect()))
assert "FROM dkb.correction" in sql
assert "osv_entry" not in sql
assert "correction.consolidation_version_id" in sql
assert "correction.rule_id" in sql
assert "correction.status" in sql
async def test_correction_query_requires_version_and_rule() -> None:
session = AsyncMock()
assert await correction_for_version_consolidation(session, None, uuid.uuid4()) == []
assert await correction_for_version_consolidation(session, uuid.uuid4(), None) == []
session.scalars.assert_not_awaited()

View File

@ -82,7 +82,6 @@ def test_routes_are_separate_and_matrix_expands_manual_accounts() -> None:
] ]
assert result.cells[0].amount_a == Decimal("100") assert result.cells[0].amount_a == Decimal("100")
assert result.cells[0].amount_b == Decimal("90") assert result.cells[0].amount_b == Decimal("90")
assert result.cells[0].correction_ids_debit == result.cells[0].correction_ids_credit == []
assert result.cells[0].variance_percent == Decimal("11.11") assert result.cells[0].variance_percent == Decimal("11.11")
assert isinstance(result, IntercompanyMatrixOut) assert isinstance(result, IntercompanyMatrixOut)
response = result response = result
@ -143,7 +142,6 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey
monkeypatch.setattr( monkeypatch.setattr(
matrix.elimination_rules_repo, "rule_with_account", AsyncMock(return_value=(rule, rule_accounts)) matrix.elimination_rules_repo, "rule_with_account", AsyncMock(return_value=(rule, rule_accounts))
) )
monkeypatch.setattr(matrix.consolidation_repo, "correction_for_version_consolidation", AsyncMock(return_value=[]))
session = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace(scalar_one=lambda: "ГК"))) session = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace(scalar_one=lambda: "ГК")))
service = matrix.IntercompanyMatrixService(session, None, osv_service) service = matrix.IntercompanyMatrixService(session, None, osv_service)
captured = [] captured = []
@ -164,79 +162,6 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey
assert info.measure == "turnover" assert info.measure == "turnover"
async def test_preset_corrections_exclude_amounts_without_changing_osv(monkeypatch: pytest.MonkeyPatch) -> None:
company_a, company_b = uuid.uuid4(), uuid.uuid4()
debit_account, credit_account = uuid.uuid4(), uuid.uuid4()
debit_row = _row(company_a, company_b, debit_account, debit="1500")
credit_row = _row(company_b, company_a, credit_account, credit="1500")
debit_row.entry_ids = (uuid.uuid4(),)
credit_row.entry_ids = (uuid.uuid4(),)
companies = [
IntercompanyMatrixCompanyOut(id=company_a, name="Компания А"),
IntercompanyMatrixCompanyOut(id=company_b, name="Компания Б"),
]
original = matrix.calculate_matrix(
[debit_row, credit_row],
companies,
matrix.MatrixSelection(frozenset({debit_account}), frozenset({credit_account}), "closing"),
)
debit_correction_id, credit_correction_id, child_correction_id = uuid.uuid4(), uuid.uuid4(), uuid.uuid4()
corrections = [
SimpleNamespace(
id=debit_correction_id,
legal_entity_id=company_a,
counterparty_id=company_b,
debit_adjustment=Decimal("1000"),
credit_adjustment=None,
),
SimpleNamespace(
id=credit_correction_id,
legal_entity_id=company_b,
counterparty_id=company_a,
debit_adjustment=None,
credit_adjustment=Decimal("500"),
),
SimpleNamespace(
id=child_correction_id,
legal_entity_id=company_b,
counterparty_id=company_a,
debit_adjustment=None,
credit_adjustment=Decimal("500"),
),
]
monkeypatch.setattr(
matrix.consolidation_repo, "correction_for_version_consolidation", AsyncMock(return_value=corrections)
)
adjusted = await matrix.IntercompanyMatrixService(object(), None, object())._apply_corrections(
original, uuid.uuid4(), uuid.uuid4()
)
assert adjusted.cells[0].amount_a == Decimal("500")
assert adjusted.cells[0].amount_b == Decimal("500")
assert adjusted.cells[0].variance_amount == Decimal("0")
assert adjusted.cells[0].osv_entry_ids_debit == [debit_row.entry_ids[0]]
assert adjusted.cells[0].osv_entry_ids_credit == [credit_row.entry_ids[0]]
assert adjusted.cells[0].correction_ids_debit == [debit_correction_id]
assert set(adjusted.cells[0].correction_ids_credit) == {credit_correction_id, child_correction_id}
assert adjusted.cells[0].model_dump(mode="json")["correction_ids_debit"] == [str(debit_correction_id)]
assert original.cells[0].correction_ids_debit == original.cells[0].correction_ids_credit == []
assert original.cells[0].amount_a == Decimal("1500")
assert original.cells[0].amount_b == Decimal("1500")
assert credit_row.closing_credit == Decimal("1500")
async def test_preset_without_corrections_keeps_matrix(monkeypatch: pytest.MonkeyPatch) -> None:
original = matrix.IntercompanyMatrixOut(companies=[], cells=[])
monkeypatch.setattr(matrix.consolidation_repo, "correction_for_version_consolidation", AsyncMock(return_value=[]))
adjusted = await matrix.IntercompanyMatrixService(object(), None, object())._apply_corrections(
original, uuid.uuid4(), uuid.uuid4()
)
assert adjusted is original
def test_one_sided_reflects_osv_sources_even_when_amount_is_zero() -> None: def test_one_sided_reflects_osv_sources_even_when_amount_is_zero() -> None:
company_a, company_b = uuid.uuid4(), uuid.uuid4() company_a, company_b = uuid.uuid4(), uuid.uuid4()
debit_account, credit_account = uuid.uuid4(), uuid.uuid4() debit_account, credit_account = uuid.uuid4(), uuid.uuid4()

View File

@ -134,14 +134,6 @@ def test_matrix_export_has_three_rows_per_debit_company_and_highlights_threshold
for offset in range(4) for offset in range(4)
) )
debit_correction_id = UUID("796d1add-eaed-4802-8c79-8b3cdae2cca1")
credit_correction_id = UUID("55555555-5555-5555-5555-555555555555")
matrix.cells[0].correction_ids_debit = [debit_correction_id]
matrix.cells[0].correction_ids_credit = [credit_correction_id]
preset_sheet = load_workbook(BytesIO(build_matrix_workbook(matrix, _info(mode="PRESET")))).active
assert preset_sheet is not None
assert preset_sheet["D12"].comment is None
assert preset_sheet["D13"].comment is None
async def test_export_routes_use_matrix_calculation() -> None: async def test_export_routes_use_matrix_calculation() -> None:

View File

@ -37,7 +37,7 @@ change не входит. Шахматка — чистая функция от
3. **Корректировки из шахматки убираются** (решение 4 выше и `correction_ids_*` в ячейке). Причины: корректировки в 3. **Корректировки из шахматки убираются** (решение 4 выше и `correction_ids_*` в ячейке). Причины: корректировки в
ядре никто не создаёт (спринт корректировок), семантика ключа `(rule, A, B)` относительно ячейки (A, B) / (B, A) не ядре никто не создаёт (спринт корректировок), семантика ключа `(rule, A, B)` относительно ячейки (A, B) / (B, A) не
утверждена, счета корректировки и `parent_correction_id` не учитывались; §6 описывает шахматку как суммы первички. утверждена, счета корректировки и `parent_correction_id` не учитывались; §6 описывает шахматку как суммы первички.
Код сохранён в ветке `wip/shahmatka-corrections-unpaired` (`0e95f95`) и возвращается после решения по аналитике Код корректировок выделен в ветку `AURORA-1747` и возвращается после решения по аналитике
AURORA-1643 «Учитывание корректировок в шахматке». AURORA-1643 «Учитывание корректировок в шахматке».
4. **Ручка `…/preset/{rule_id}/unpaired-osv` убирается** (решение 5 выше). Причины: «без пары» по наличию строк 4. **Ручка `…/preset/{rule_id}/unpaired-osv` убирается** (решение 5 выше). Причины: «без пары» по наличию строк
(дефект AURORA-1651), ложные срабатывания при пересечении счетов сторон (76 в правиле № 6), нет ячейки и стороны (дефект AURORA-1651), ложные срабатывания при пересечении счетов сторон (76 в правиле № 6), нет ячейки и стороны

View File

@ -1,7 +1,7 @@
## 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`, и в спеке не закреплено как окончательное.
@ -10,8 +10,7 @@
- Режим 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`. строк без пары `…/preset/{rule_id}/unpaired-osv`.
- Показатель пресета (`closing` / `turnover`) берётся из правила; в ячейке появляются `correction_ids_debit/credit`, - Показатель пресета (`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`).
- xlsx: для PRESET имя файла с `_preset-<rule_id>`, в шапке строка «Показатель»; audit `EXPORT` пишет `mode` - xlsx: для PRESET имя файла с `_preset-<rule_id>`, в шапке строка «Показатель»; audit `EXPORT` пишет `mode`
@ -24,11 +23,11 @@
### Modified Capabilities ### Modified Capabilities
- `consolidations-api`: ответ правил элиминации версии. - `consolidations-api`: ответ правил элиминации версии.
- `consolidated-osv`: режим PRESET шахматки, корректировки, строки без пары, xlsx пресета. - `consolidated-osv`: режим PRESET шахматки, строки без пары, xlsx пресета.
- `audit-trail`: аудит экспорта шахматки (режим PRESET). - `audit-trail`: аудит экспорта шахматки (режим PRESET).
## Impact ## Impact
Только `arm-core` (API, сервисы, схемы), таблицы и DBML не меняются. Ломающее изменение контракта Только `arm-core` (API, сервисы, схемы), таблицы и DBML не меняются. Ломающее изменение контракта
`GET …/elimination-rules` (массив → объект); поле `as_of` в ответ не входит. Потребителей старого формата в этом репозитории не проверялось — проверить при слиянии. Non-goals: фиксация версии правил, расчёт «актуальных» правил на отчётную дату, создание корректировок, `GET …/elimination-rules` (массив → объект); поле `as_of` в ответ не входит. Потребителей старого формата в этом репозитории не проверялось — проверить при слиянии. Non-goals: фиксация версии правил, расчёт «актуальных» правил на отчётную дату, создание и применение корректировок,
исправление `one_sided` (AURORA-1651). исправление `one_sided` (AURORA-1651).

View File

@ -28,26 +28,6 @@
- **WHEN** `rule_id` не существует - **WHEN** `rule_id` не существует
- **THEN** 404 - **THEN** 404
### Requirement: Корректировки в шахматке пресета
В режиме PRESET расчёт SHALL учитывать корректировки `correction` со статусом `APPLIED`, у которых
`consolidation_version_id` равен версии, а `rule_id` — правилу пресета. Корректировка с `legal_entity_id = A` и
`counterparty_id = B` SHALL уменьшать `amount_a` ячейки `(A, B)` на `debit_adjustment` и `amount_b` ячейки `(B, A)` на
`credit_adjustment`. Затронутые ячейки SHALL получать пересчитанные `variance_amount`, `variance_percent`,
`exceeds_threshold` и ID корректировок в `correction_ids_debit`/`correction_ids_credit`; у остальных ячеек эти массивы
пусты (поля есть в ответе всех режимов). Корректировки без `legal_entity_id` или `counterparty_id` SHALL игнорироваться.
#### Scenario: Корректировка по дебету
- **WHEN** есть применённая корректировка правила `(A, B)` с `debit_adjustment = 10`
- **THEN** у ячейки `(A, B)` `amount_a` меньше на 10, расхождение пересчитано, `correction_ids_debit` содержит её ID
#### Scenario: Корректировка по кредиту
- **WHEN** есть применённая корректировка правила `(A, B)` с `credit_adjustment = 5`
- **THEN** у ячейки `(B, A)` `amount_b` меньше на 5, `correction_ids_credit` содержит её ID
#### Scenario: Корректировка чужого правила или не применённая
- **WHEN** корректировка относится к другому правилу либо имеет статус не `APPLIED`
- **THEN** суммы ячеек не меняются, `correction_ids_*` пусты
### Requirement: Строки сводной ОСВ без пары по пресету ### Requirement: Строки сводной ОСВ без пары по пресету
`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}/unpaired-osv`
(`CONSOLIDATION_VIEW`, policy) SHALL возвращать `{items, total}`: все строки сводной ОСВ версии, чьи записи ОСВ (`CONSOLIDATION_VIEW`, policy) SHALL возвращать `{items, total}`: все строки сводной ОСВ версии, чьи записи ОСВ

View File

@ -5,8 +5,9 @@
## 2. Доработки по решениям владельца 2026-10-21 (автор) ## 2. Доработки по решениям владельца 2026-10-21 (автор)
- [ ] 2.1 Отдельным коммитом убрать учёт корректировок (`_apply_corrections`, `correction_for_version_consolidation`, - [x] 2.1 Отдельным коммитом убрать учёт корректировок (`_apply_corrections`, `correction_for_version_consolidation`,
`correction_ids_*`) и ручку `unpaired-osv` (`get_unpaired_rows`, `UnpairedOsvOut`, маршрут) вместе с тестами; `correction_ids_*`) вместе с тестами; код выделен в `AURORA-1747`.
- [ ] 2.1a Убрать ручку `unpaired-osv` (`get_unpaired_rows`, `UnpairedOsvOut`, маршрут) вместе с тестами;
код сохранён в `wip/shahmatka-corrections-unpaired`. код сохранён в `wip/shahmatka-corrections-unpaired`.
- [ ] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест. - [ ] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест.
- [ ] 2.3 Несколько открытых `version_no` — одна версия (max) или явная ошибка, тест. - [ ] 2.3 Несколько открытых `version_no` — одна версия (max) или явная ошибка, тест.