fix(matrix): выбирать последнюю открытую версию правил
This commit is contained in:
parent
38c8eefa18
commit
3eb5362e58
@ -874,7 +874,7 @@ API: список счетов `GET /directories/accounts`, правила ве
|
|||||||
`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`.
|
`…/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`) с максимальным `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`) и `rule_id`. Открытые вопросы по пресетам —
|
||||||
`openspec/changes/intercompany-matrix-presets/design.md`. Контракт для фронта —
|
`openspec/changes/intercompany-matrix-presets/design.md`. Контракт для фронта —
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Collection
|
from collections.abc import Collection
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.models.accounting import ChartOfAccount, EliminationRule, EliminationRuleAccount
|
from src.models.accounting import ChartOfAccount, EliminationRule, EliminationRuleAccount
|
||||||
@ -12,9 +12,14 @@ from src.models.accounting import ChartOfAccount, EliminationRule, EliminationRu
|
|||||||
async def list_rules_with_accounts(
|
async def list_rules_with_accounts(
|
||||||
session: AsyncSession, version_no: int | None
|
session: AsyncSession, version_no: int | None
|
||||||
) -> list[tuple[EliminationRule, EliminationRuleAccount | None]]:
|
) -> list[tuple[EliminationRule, EliminationRuleAccount | None]]:
|
||||||
"""Правила заданной версии либо все правила без `valid_to`."""
|
"""Правила заданной версии либо открытые правила последней версии."""
|
||||||
condition = (
|
condition = (
|
||||||
EliminationRule.version_no == version_no if version_no is not None else EliminationRule.valid_to.is_(None)
|
EliminationRule.version_no == version_no
|
||||||
|
if version_no is not None
|
||||||
|
else (
|
||||||
|
EliminationRule.valid_to.is_(None)
|
||||||
|
& (EliminationRule.version_no == _current_open_version_stmt().scalar_subquery())
|
||||||
|
)
|
||||||
)
|
)
|
||||||
stmt = (
|
stmt = (
|
||||||
select(EliminationRule, EliminationRuleAccount)
|
select(EliminationRule, EliminationRuleAccount)
|
||||||
@ -29,6 +34,15 @@ async def list_rules_with_accounts(
|
|||||||
return [(rule, account) for rule, account in (await session.execute(stmt)).all()]
|
return [(rule, account) for rule, account in (await session.execute(stmt)).all()]
|
||||||
|
|
||||||
|
|
||||||
|
def _current_open_version_stmt():
|
||||||
|
return select(func.max(EliminationRule.version_no)).where(EliminationRule.valid_to.is_(None))
|
||||||
|
|
||||||
|
|
||||||
|
async def current_open_version_no(session: AsyncSession) -> int | None:
|
||||||
|
"""Номер последней версии, в которой есть открытые правила."""
|
||||||
|
return await session.scalar(_current_open_version_stmt())
|
||||||
|
|
||||||
|
|
||||||
def is_valid_for_version(rule: EliminationRule | None, version_no: int | None) -> bool:
|
def is_valid_for_version(rule: EliminationRule | None, version_no: int | None) -> bool:
|
||||||
"""Проверить номер версии либо отсутствие `valid_to`."""
|
"""Проверить номер версии либо отсутствие `valid_to`."""
|
||||||
return rule is not None and (rule.version_no == version_no if version_no is not None else rule.valid_to is None)
|
return rule is not None and (rule.version_no == version_no if version_no is not None else rule.valid_to is None)
|
||||||
|
|||||||
@ -28,6 +28,8 @@ class EliminationRuleService:
|
|||||||
is_version_fixed = version.elimination_rules_version is not None
|
is_version_fixed = version.elimination_rules_version is not None
|
||||||
version_no = version.elimination_rules_version
|
version_no = version.elimination_rules_version
|
||||||
rows = await rules_repo.list_rules_with_accounts(self._session, version_no)
|
rows = await rules_repo.list_rules_with_accounts(self._session, version_no)
|
||||||
|
if not is_version_fixed and rows:
|
||||||
|
version_no = rows[0][0].version_no
|
||||||
by_id: dict[uuid.UUID, EliminationRuleOut] = {}
|
by_id: dict[uuid.UUID, EliminationRuleOut] = {}
|
||||||
for rule, account in rows:
|
for rule, account in rows:
|
||||||
result = by_id.get(rule.id)
|
result = by_id.get(rule.id)
|
||||||
|
|||||||
@ -419,6 +419,10 @@ class IntercompanyMatrixService:
|
|||||||
rule, rule_accounts = await elimination_rules_repo.rule_with_account(self._session, rule_id)
|
rule, rule_accounts = await elimination_rules_repo.rule_with_account(self._session, rule_id)
|
||||||
if rule is None or not elimination_rules_repo.is_valid_for_version(rule, version.elimination_rules_version):
|
if rule is None or not elimination_rules_repo.is_valid_for_version(rule, version.elimination_rules_version):
|
||||||
raise NotFoundError("правило-пресет не найдено для версии консолидации")
|
raise NotFoundError("правило-пресет не найдено для версии консолидации")
|
||||||
|
if version.elimination_rules_version is None:
|
||||||
|
open_version_no = await elimination_rules_repo.current_open_version_no(self._session)
|
||||||
|
if rule.version_no != open_version_no:
|
||||||
|
raise NotFoundError("правило-пресет не найдено для версии консолидации")
|
||||||
if rule.statement_type != StatementType.balance:
|
if rule.statement_type != StatementType.balance:
|
||||||
raise DomainValidationError("INVALID_RULE", "Шахматка PRESET поддерживает только правила ББ")
|
raise DomainValidationError("INVALID_RULE", "Шахматка PRESET поддерживает только правила ББ")
|
||||||
if rule.side_1 not in ("D", "K") or rule.measure not in ("closing", "turnover"):
|
if rule.side_1 not in ("D", "K") or rule.measure not in ("closing", "turnover"):
|
||||||
|
|||||||
@ -235,7 +235,7 @@ async def test_preset_uses_current_rules_when_version_unfixed(
|
|||||||
listed = await client.get(f"/consolidations/versions/{world.version.id}/elimination-rules")
|
listed = await client.get(f"/consolidations/versions/{world.version.id}/elimination-rules")
|
||||||
assert listed.status_code == OK, listed.text
|
assert listed.status_code == OK, listed.text
|
||||||
assert listed.json()["is_version_fixed"] is False
|
assert listed.json()["is_version_fixed"] is False
|
||||||
assert listed.json()["version_no"] is None
|
assert listed.json()["version_no"] == rule.version_no
|
||||||
assert "as_of" not in listed.json()
|
assert "as_of" not in listed.json()
|
||||||
assert str(rule.id) in {item["id"] for item in listed.json()["rules"]}
|
assert str(rule.id) in {item["id"] for item in listed.json()["rules"]}
|
||||||
response = await client.get(url)
|
response = await client.get(url)
|
||||||
@ -258,6 +258,30 @@ async def test_preset_uses_current_rules_when_version_unfixed(
|
|||||||
assert (await client.get(f"{missing_url}/export")).status_code == NOT_FOUND
|
assert (await client.get(f"{missing_url}/export")).status_code == NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unfixed_rules_choose_latest_open_version(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||||
|
world = await _world(seeded)
|
||||||
|
old_rule, new_rule = (
|
||||||
|
await seeded.scalars(select(EliminationRule).order_by(EliminationRule.rule_no).limit(2))
|
||||||
|
).all()
|
||||||
|
assert old_rule.valid_to is None and new_rule.valid_to is None
|
||||||
|
new_rule.rule_no = old_rule.rule_no
|
||||||
|
new_rule.version_no = old_rule.version_no + 1
|
||||||
|
await seeded.flush()
|
||||||
|
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
base = f"/consolidations/versions/{world.version.id}"
|
||||||
|
response = await client.get(f"{base}/elimination-rules")
|
||||||
|
assert response.status_code == OK, response.text
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["is_version_fixed"] is False
|
||||||
|
assert payload["version_no"] == new_rule.version_no
|
||||||
|
assert [item["id"] for item in payload["rules"]] == [str(new_rule.id)]
|
||||||
|
assert payload["rules"][0]["rule_no"] == old_rule.rule_no
|
||||||
|
|
||||||
|
old_preset = await client.get(f"{base}/intercompany-matrix/preset/{old_rule.id}")
|
||||||
|
assert old_preset.status_code == NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
async def test_unmapped_account_source_is_visible_in_cell_count(
|
async def test_unmapped_account_source_is_visible_in_cell_count(
|
||||||
auth_client: AuthClientFactory, seeded: AsyncSession
|
auth_client: AuthClientFactory, seeded: AsyncSession
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@ -42,7 +42,10 @@ async def test_rules_filter_by_version_or_open_valid_to() -> None:
|
|||||||
session = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace(all=lambda: [])))
|
session = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace(all=lambda: [])))
|
||||||
|
|
||||||
await rules_repo.list_rules_with_accounts(session, None)
|
await rules_repo.list_rules_with_accounts(session, None)
|
||||||
assert "valid_to IS NULL" in str(session.execute.await_args.args[0])
|
query = str(session.execute.await_args.args[0])
|
||||||
|
assert "valid_to IS NULL" in query
|
||||||
|
assert "max(" in query
|
||||||
|
assert "version_no = (SELECT" in query
|
||||||
|
|
||||||
await rules_repo.list_rules_with_accounts(session, version_no)
|
await rules_repo.list_rules_with_accounts(session, version_no)
|
||||||
assert "valid_to IS NULL" not in str(session.execute.await_args.args[0])
|
assert "valid_to IS NULL" not in str(session.execute.await_args.args[0])
|
||||||
@ -87,7 +90,7 @@ async def test_unfixed_rules_use_open_valid_to(monkeypatch: pytest.MonkeyPatch)
|
|||||||
result = await service_module.EliminationRuleService(session, scope).list_rules(version_id)
|
result = await service_module.EliminationRuleService(session, scope).list_rules(version_id)
|
||||||
|
|
||||||
assert result.is_version_fixed is False
|
assert result.is_version_fixed is False
|
||||||
assert result.version_no is None
|
assert result.version_no == rule.version_no
|
||||||
assert [item.id for item in result.rules] == [rule.id]
|
assert [item.id for item in result.rules] == [rule.id]
|
||||||
listed.assert_awaited_once_with(session, None)
|
listed.assert_awaited_once_with(session, None)
|
||||||
|
|
||||||
|
|||||||
@ -157,6 +157,9 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey
|
|||||||
assert captured[0].measure == "turnover"
|
assert captured[0].measure == "turnover"
|
||||||
|
|
||||||
version.elimination_rules_version = None
|
version.elimination_rules_version = None
|
||||||
|
monkeypatch.setattr(
|
||||||
|
matrix.elimination_rules_repo, "current_open_version_no", AsyncMock(return_value=rule.version_no)
|
||||||
|
)
|
||||||
_, info = await service.build_export(uuid.uuid4(), mode="PRESET", rule_id=uuid.uuid4())
|
_, info = await service.build_export(uuid.uuid4(), mode="PRESET", rule_id=uuid.uuid4())
|
||||||
assert info.mode == "PRESET"
|
assert info.mode == "PRESET"
|
||||||
assert info.debit_account_codes == ("76",)
|
assert info.debit_account_codes == ("76",)
|
||||||
|
|||||||
@ -10,7 +10,8 @@ change не входит. Шахматка — чистая функция от
|
|||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
1. Режим PRESET берёт правило по `rule_id`. Правило должно быть действительно для версии: при зафиксированном
|
1. Режим PRESET берёт правило по `rule_id`. Правило должно быть действительно для версии: при зафиксированном
|
||||||
`elimination_rules_version` — совпадать по `version_no`, иначе — иметь `valid_to IS NULL`; иначе 404.
|
`elimination_rules_version` — совпадать по `version_no`, иначе — иметь `valid_to IS NULL` и максимальный открытый
|
||||||
|
`version_no`; иначе 404.
|
||||||
2. Сторона дебета — `company_1`, если `side_1 = D`, иначе `company_2`; кредит — вторая сторона. Счета и исключения
|
2. Сторона дебета — `company_1`, если `side_1 = D`, иначе `company_2`; кредит — вторая сторона. Счета и исключения
|
||||||
раскрываются по иерархии (`_select_accounts`), как в MANUAL. Нет счетов хотя бы одной стороны — 422
|
раскрываются по иерархии (`_select_accounts`), как в MANUAL. Нет счетов хотя бы одной стороны — 422
|
||||||
`ACCOUNTS_REQUIRED`; неверные `side_1`/`measure`/`company_no` — 422 `INVALID_RULE`; не передан `rule_id` — 422
|
`ACCOUNTS_REQUIRED`; неверные `side_1`/`measure`/`company_no` — 422 `INVALID_RULE`; не передан `rule_id` — 422
|
||||||
@ -50,11 +51,7 @@ change не входит. Шахматка — чистая функция от
|
|||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
1. Если открытыми остаются несколько `version_no`, список правил даёт дубли `rule_no` (обязательно в PR: одна версия
|
1. Имя файла содержит UUID правила, шапка xlsx не содержит номер правила и версию; audit EXPORT не пишет номер и
|
||||||
или явная ошибка, тест).
|
|
||||||
2. При незафиксированной версии ответ `elimination-rules` отдаёт `version_no = null` — фронт не видит, по какой версии
|
|
||||||
посчитано (желательно отдавать фактический номер открытых правил).
|
|
||||||
3. Имя файла содержит UUID правила, шапка xlsx не содержит номер правила и версию; audit EXPORT не пишет номер и
|
|
||||||
версию правил.
|
версию правил.
|
||||||
|
|
||||||
## Decisions made without discussion
|
## Decisions made without discussion
|
||||||
@ -65,9 +62,8 @@ change не входит. Шахматка — чистая функция от
|
|||||||
автор и владелец, не ревьюер. Цена ошибки: после решения владельца спека меняется дельтой.
|
автор и владелец, не ревьюер. Цена ошибки: после решения владельца спека меняется дельтой.
|
||||||
- 2026-10-21. Поле `as_of` удалено из `EliminationRulesOut` как мёртвое: после правок по ревью всегда `None`, в main
|
- 2026-10-21. Поле `as_of` удалено из `EliminationRulesOut` как мёртвое: после правок по ревью всегда `None`, в main
|
||||||
его не было, потребителей нет. Цена ошибки: если фронт ждал `as_of`, поле нужно вернуть.
|
его не было, потребителей нет. Цена ошибки: если фронт ждал `as_of`, поле нужно вернуть.
|
||||||
- 2026-10-21. Интеграционный тест `test_preset_uses_current_rules_when_version_unfixed` приведён к фактическому
|
- 2026-10-21. При незаданной версии правил выбирается максимальный открытый `version_no`; он возвращается в ответе
|
||||||
поведению (`version_no is None`, `is_version_fixed is False`), а не к старому ожиданию `version_no == rule.version_no`;
|
`elimination-rules`, чтобы фронт видел фактически применённую версию.
|
||||||
логика вычисления `version_no` не добавлялась. Цена ошибки: если автор решит возвращать номер версии, тест вернуть.
|
|
||||||
- 2026-10-21. mypy: `measure` аннотирован `Measure` без `cast`; слишком длинная f-строка имени файла свода ОСВ
|
- 2026-10-21. mypy: `measure` аннотирован `Measure` без `cast`; слишком длинная f-строка имени файла свода ОСВ
|
||||||
(`consolidated_osv.py`) разбита на две без изменения результата (ruff E501). Цена ошибки: нулевая.
|
(`consolidated_osv.py`) разбита на две без изменения результата (ruff E501). Цена ошибки: нулевая.
|
||||||
- 2026-10-21. В MODIFIED-блоке требования про правила элиминации и аудит экспорта приведён полный текст; требование
|
- 2026-10-21. В MODIFIED-блоке требования про правила элиминации и аудит экспорта приведён полный текст; требование
|
||||||
|
|||||||
@ -12,7 +12,8 @@
|
|||||||
строк без пары `…/preset/{rule_id}/unpaired-osv`.
|
строк без пары `…/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`)
|
||||||
|
с максимальным `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` и `rule_id`.
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
раскрываются по иерархии и фильтруются по виду, как в MANUAL. Показатель SHALL браться из `measure` правила:
|
раскрываются по иерархии и фильтруются по виду, как в MANUAL. Показатель SHALL браться из `measure` правила:
|
||||||
`closing` — конечное сальдо (`closing_debit`/`closing_credit`), `turnover` — обороты (`turnover_debit`/`turnover_credit`).
|
`closing` — конечное сальдо (`closing_debit`/`closing_credit`), `turnover` — обороты (`turnover_debit`/`turnover_credit`).
|
||||||
Правило SHALL быть действительным для версии консолидации: при заданном `elimination_rules_version` — с тем же
|
Правило SHALL быть действительным для версии консолидации: при заданном `elimination_rules_version` — с тем же
|
||||||
`version_no`, иначе — без `valid_to`; несуществующее или недействительное правило SHALL давать 404. Ошибки 422:
|
`version_no`, иначе — без `valid_to` и с максимальным открытым `version_no`; несуществующее или недействительное правило SHALL давать 404. Ошибки 422:
|
||||||
`RULE_REQUIRED` — правило не выбрано, `INVALID_RULE` — правило ОФР либо неверные `side_1`, `measure` или сторона счёта правила,
|
`RULE_REQUIRED` — правило не выбрано, `INVALID_RULE` — правило ОФР либо неверные `side_1`, `measure` или сторона счёта правила,
|
||||||
`ACCOUNTS_REQUIRED` — у одной из сторон нет счетов. Невидимая и несуществующая версия SHALL отвечать 404 `NOT_FOUND`.
|
`ACCOUNTS_REQUIRED` — у одной из сторон нет счетов. Невидимая и несуществующая версия SHALL отвечать 404 `NOT_FOUND`.
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,9 @@
|
|||||||
`GET /api/v1/consolidations/versions/{version_id}/elimination-rules` (`CONSOLIDATION_VIEW`, policy по ГК)
|
`GET /api/v1/consolidations/versions/{version_id}/elimination-rules` (`CONSOLIDATION_VIEW`, policy по ГК)
|
||||||
SHALL возвращать объект `{version_no, is_version_fixed, rules}`. Если у версии задан
|
SHALL возвращать объект `{version_no, is_version_fixed, rules}`. Если у версии задан
|
||||||
`consolidation_version.elimination_rules_version`, `version_no` SHALL равняться ему, `is_version_fixed = true`, а
|
`consolidation_version.elimination_rules_version`, `version_no` SHALL равняться ему, `is_version_fixed = true`, а
|
||||||
`rules` — правилами этой версии. Если он не задан, `version_no = null`, `is_version_fixed = false`, а `rules` —
|
`rules` — правилами этой версии. Если он не задан, `is_version_fixed = false`, а `rules` —
|
||||||
открытые правила (`valid_to IS NULL`); при их отсутствии `rules = []`. Каждое правило SHALL содержать `id`, `rule_no`,
|
открытые правила (`valid_to IS NULL`) только с максимальным `version_no`; ответ SHALL содержать этот фактический
|
||||||
|
`version_no`. При отсутствии открытых правил `version_no = null`, `rules = []`. Каждое правило SHALL содержать `id`, `rule_no`,
|
||||||
`version_no`, `statement_type`, `side_1` (`D`/`K`), `measure` (`closing`/`turnover`), `company_1`, `company_2`;
|
`version_no`, `statement_type`, `side_1` (`D`/`K`), `measure` (`closing`/`turnover`), `company_1`, `company_2`;
|
||||||
каждая сторона SHALL содержать `accounts: UUID[]` и `excluded: UUID[]` до раскрытия иерархии счетов.
|
каждая сторона SHALL содержать `accounts: UUID[]` и `excluded: UUID[]` до раскрытия иерархии счетов.
|
||||||
Невидимая и несуществующая версия SHALL отвечать одинаковым 404 `NOT_FOUND`.
|
Невидимая и несуществующая версия SHALL отвечать одинаковым 404 `NOT_FOUND`.
|
||||||
@ -20,4 +21,8 @@ SHALL возвращать объект `{version_no, is_version_fixed, rules}`.
|
|||||||
|
|
||||||
#### Scenario: Версия правил не зафиксирована
|
#### Scenario: Версия правил не зафиксирована
|
||||||
- **WHEN** у версии консолидации `elimination_rules_version` пуст
|
- **WHEN** у версии консолидации `elimination_rules_version` пуст
|
||||||
- **THEN** ответ содержит `is_version_fixed = false`, `version_no = null` и открытые правила с `valid_to IS NULL`
|
- **THEN** ответ содержит `is_version_fixed = false`, фактический `version_no` и открытые правила только этой версии
|
||||||
|
|
||||||
|
#### Scenario: Открыты две версии правил
|
||||||
|
- **WHEN** открыты правила с `version_no = 1` и `version_no = 2`, в том числе с одинаковым `rule_no`
|
||||||
|
- **THEN** ответ содержит только правила версии 2 и `version_no = 2`
|
||||||
|
|||||||
@ -10,5 +10,5 @@
|
|||||||
- [ ] 2.1a Убрать ручку `unpaired-osv` (`get_unpaired_rows`, `UnpairedOsvOut`, маршрут) вместе с тестами;
|
- [ ] 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`, тест.
|
||||||
- [ ] 2.3 Несколько открытых `version_no` — одна версия (max) или явная ошибка, тест.
|
- [x] 2.3 Несколько открытых `version_no` — только максимальная версия; вернуть её номер, тест.
|
||||||
- [ ] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README.
|
- [ ] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user