diff --git a/api/src/repositories/elimination_rules.py b/api/src/repositories/elimination_rules.py index d059395..06fa06d 100644 --- a/api/src/repositories/elimination_rules.py +++ b/api/src/repositories/elimination_rules.py @@ -2,6 +2,7 @@ import uuid from collections.abc import Collection +from datetime import date from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -44,6 +45,16 @@ async def current_open_version_no(session: AsyncSession) -> int | None: return await session.scalar(_current_open_version_stmt()) +async def latest_effective_version_no(session: AsyncSession, as_of: date) -> int | None: + """Максимальный номер версии правил, действующей на дату создания""" + return await session.scalar( + select(func.max(EliminationRule.version_no)).where( + EliminationRule.valid_from <= as_of, + (EliminationRule.valid_to.is_(None) | (EliminationRule.valid_to >= as_of)), + ) + ) + + def is_valid_for_version(rule: EliminationRule | None, version_no: int | None) -> bool: """Проверить версию правила §4 либо отсутствие `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) diff --git a/api/src/services/consolidations.py b/api/src/services/consolidations.py index 3d05f0c..19b0443 100644 --- a/api/src/services/consolidations.py +++ b/api/src/services/consolidations.py @@ -16,7 +16,7 @@ from collections.abc import Sequence from sqlalchemy.ext.asyncio import AsyncSession from src.api.dependencies.audit import RequestAudit -from src.common.clock import utc_now +from src.common.clock import moscow_date, utc_now from src.common.exceptions import ConflictError, DomainValidationError, NotFoundError from src.domain import checklist as checklist_domain from src.domain.consolidation_periods import quarters_through, reporting_date @@ -40,6 +40,7 @@ from src.models.enums import ( ) from src.repositories import consolidations as repo from src.repositories import directories as directories_repo +from src.repositories import elimination_rules as elimination_rules_repo from src.repositories.users import UserWithAccess from src.schemas.consolidations import ( ChecklistCellOut, @@ -347,6 +348,9 @@ class ConsolidationService: scenario=ConsolidationScenario.CREATE, approach=ConsolidationApproach.BASIC, goodwill_enabled=False, + elimination_rules_version=await elimination_rules_repo.latest_effective_version_no( + self._session, moscow_date(now) + ), initiator_id=user.id, initiator_office_id=user.regional_office_id, is_in_work=True, diff --git a/api/tests/test_api_consolidations.py b/api/tests/test_api_consolidations.py index 910ef67..0fa51a0 100644 --- a/api/tests/test_api_consolidations.py +++ b/api/tests/test_api_consolidations.py @@ -4,13 +4,16 @@ import uuid from collections.abc import Sequence +from datetime import timedelta from typing import Any import pytest from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from src.common.clock import moscow_date, utc_now from src.models.access import AppUser +from src.models.accounting import EliminationRule from src.models.audit_ex import AuditEvent from src.models.consolidation import ( Consolidation, @@ -139,6 +142,7 @@ async def test_create_consolidation_rb_q3_2024(auth_client: AuthClientFactory, s assert str(consolidation.id) == card["consolidation_id"] version = (await seeded.execute(select(ConsolidationVersion))).scalar_one() assert (version.initiator_id, version.created_by, version.is_in_work) == (user.id, user.id, True) + assert version.elimination_rules_version == 1 assert await _count(seeded, ConsolidationLock) == 0 assert await _count(seeded, ConsolidationMember) == len(entities) assert await _count(seeded, ConsolidationStatusHistory) == 1 @@ -162,6 +166,44 @@ async def test_create_consolidation_rb_q3_2024(auth_client: AuthClientFactory, s } +@pytest.mark.integration +async def test_create_fixes_rules_for_creation_date_not_reporting_period( + auth_client: AuthClientFactory, seeded: AsyncSession +) -> None: + group, gsk, _ = await _scenario(seeded) + today = moscow_date(utc_now()) + source = (await seeded.scalars(select(EliminationRule).where(EliminationRule.rule_no == 1))).one() + next_rule = EliminationRule( + statement_type=source.statement_type, + rule_no=source.rule_no, + article_1_line=source.article_1_line, + article_2_line=source.article_2_line, + side_1=source.side_1, + measure=source.measure, + elimination_mode=source.elimination_mode, + excise_adjust=source.excise_adjust, + version_no=2, + valid_from=today + timedelta(days=1), + ) + seeded.add(next_rule) + await seeded.flush() + client = await auth_client(KA_RF) + + first = await client.post(CONSOLIDATIONS, json=_body(group, gsk, year=2024)) + assert first.status_code == HTTP_CREATED, first.text + first_version = await seeded.get(ConsolidationVersion, uuid.UUID(first.json()["id"])) + assert first_version is not None + assert first_version.elimination_rules_version == 1 + + next_rule.valid_from = today + second = await client.post(CONSOLIDATIONS, json=_body(group, gsk, year=2023)) + assert second.status_code == HTTP_CREATED, second.text + second_version = await seeded.get(ConsolidationVersion, uuid.UUID(second.json()["id"])) + assert second_version is not None + assert second_version.elimination_rules_version == next_rule.version_no + assert first_version.elimination_rules_version == 1 + + @pytest.mark.integration async def test_create_reporting_date_for_q4_is_next_year(auth_client: AuthClientFactory, seeded: AsyncSession) -> None: group, gsk, _ = await _scenario(seeded) diff --git a/openspec/specs/consolidations-api/spec.md b/openspec/specs/consolidations-api/spec.md index abd8832..1ba4f24 100644 --- a/openspec/specs/consolidations-api/spec.md +++ b/openspec/specs/consolidations-api/spec.md @@ -15,7 +15,13 @@ Q3 → 01.10.year, Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_versi `is_in_work = true`), `consolidation_member` периметра (`is_head_entity = false`), первую запись `consolidation_status_history` (`from_status` NULL → `INITIATED`), подхват подтверждённых документов ЮЛ периметра (см. `documents-api` «Подхват документов») и одно событие `audit_event(CONSOLIDATION_CREATE)`, у которого -`after_value.members` — отсортированный список ЮЛ периметра; ответ 201 с карточкой версии. Периметр SHALL быть: без +`after_value.members` — отсортированный список ЮЛ периметра; ответ 201 с карточкой версии. +При создании версии система SHALL автоматически записать в `consolidation_version.elimination_rules_version` номер +самой актуальной версии правил: максимальный `version_no` среди правил, действующих на московскую календарную дату +создания версии (`valid_from <= дата` и `valid_to IS NULL` либо `valid_to >= дата`; OQ-174, docs PR #146). +Пользователь не выбирает версию правил. Отчётные год, квартал и `reporting_date` не влияют на определение номера. +Если на дату создания нет действующих правил, поле остаётся NULL. +Периметр SHALL быть: без `legal_entity_ids` (поле отсутствует или `null`) — текущие члены ГСК; с `legal_entity_ids` — ровно эти ЮЛ после схлопывания дублей (порядок первого вхождения), каждое SHALL принадлежать ГК консолидации (`legal_entity.company_group_id`), иначе 422 `ENTITY_NOT_IN_GROUP` с перечнем `legal_entity_ids` чужих в порядке @@ -33,6 +39,10 @@ SHALL оставаться NULL (фиксируется при верифика - **THEN** 201; версия №1 `INITIATED`, `reporting_date = 2024-10-01`, два `consolidation_member`, история с одной записью, `audit_event(CONSOLIDATION_CREATE)` с `consolidation_version_id`; активной блокировки нет +#### Scenario: Правила действуют на дату создания версии +- **WHEN** в день создания консолидации за 2024 год действует версия правил 2, а версия 1 действовала в отчётном периоде +- **THEN** `consolidation_version.elimination_rules_version = 2`; номер не меняется при последующей публикации правил + #### Scenario: Выбранный периметр шире ГСК - **WHEN** в ГК три ЮЛ, в ГСК два из них, создаётся консолидация с `legal_entity_ids` из всех трёх ЮЛ ГК - **THEN** 201; в `members` три ЮЛ; `after_value.members` события `CONSOLIDATION_CREATE` — три ЮЛ; состав ГСК не изменился