автоматическое проставление версии правил при создании консолидация, если таких создается версия консолидации создается с NULL

This commit is contained in:
PotapovaA 2026-10-23 12:10:35 +03:00
parent 22facb3ddf
commit 232fecec39
4 changed files with 69 additions and 2 deletions

View File

@ -2,6 +2,7 @@
import uuid import uuid
from collections.abc import Collection from collections.abc import Collection
from datetime import date
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession 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()) 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: def is_valid_for_version(rule: EliminationRule | None, version_no: int | None) -> bool:
"""Проверить версию правила §4 либо отсутствие `valid_to`.""" """Проверить версию правила §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) return rule is not None and (rule.version_no == version_no if version_no is not None else rule.valid_to is None)

View File

@ -16,7 +16,7 @@ from collections.abc import Sequence
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.api.dependencies.audit import RequestAudit 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.common.exceptions import ConflictError, DomainValidationError, NotFoundError
from src.domain import checklist as checklist_domain from src.domain import checklist as checklist_domain
from src.domain.consolidation_periods import quarters_through, reporting_date 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 consolidations as repo
from src.repositories import directories as directories_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.repositories.users import UserWithAccess
from src.schemas.consolidations import ( from src.schemas.consolidations import (
ChecklistCellOut, ChecklistCellOut,
@ -347,6 +348,9 @@ class ConsolidationService:
scenario=ConsolidationScenario.CREATE, scenario=ConsolidationScenario.CREATE,
approach=ConsolidationApproach.BASIC, approach=ConsolidationApproach.BASIC,
goodwill_enabled=False, goodwill_enabled=False,
elimination_rules_version=await elimination_rules_repo.latest_effective_version_no(
self._session, moscow_date(now)
),
initiator_id=user.id, initiator_id=user.id,
initiator_office_id=user.regional_office_id, initiator_office_id=user.regional_office_id,
is_in_work=True, is_in_work=True,

View File

@ -4,13 +4,16 @@
import uuid import uuid
from collections.abc import Sequence from collections.abc import Sequence
from datetime import timedelta
from typing import Any from typing import Any
import pytest import pytest
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.common.clock import moscow_date, utc_now
from src.models.access import AppUser from src.models.access import AppUser
from src.models.accounting import EliminationRule
from src.models.audit_ex import AuditEvent from src.models.audit_ex import AuditEvent
from src.models.consolidation import ( from src.models.consolidation import (
Consolidation, Consolidation,
@ -139,6 +142,7 @@ async def test_create_consolidation_rb_q3_2024(auth_client: AuthClientFactory, s
assert str(consolidation.id) == card["consolidation_id"] assert str(consolidation.id) == card["consolidation_id"]
version = (await seeded.execute(select(ConsolidationVersion))).scalar_one() 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.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, ConsolidationLock) == 0
assert await _count(seeded, ConsolidationMember) == len(entities) assert await _count(seeded, ConsolidationMember) == len(entities)
assert await _count(seeded, ConsolidationStatusHistory) == 1 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 @pytest.mark.integration
async def test_create_reporting_date_for_q4_is_next_year(auth_client: AuthClientFactory, seeded: AsyncSession) -> None: async def test_create_reporting_date_for_q4_is_next_year(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
group, gsk, _ = await _scenario(seeded) group, gsk, _ = await _scenario(seeded)

View File

@ -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`), первую запись `is_in_work = true`), `consolidation_member` периметра (`is_head_entity = false`), первую запись
`consolidation_status_history` (`from_status` NULL → `INITIATED`), подхват подтверждённых документов ЮЛ периметра (см. `consolidation_status_history` (`from_status` NULL → `INITIATED`), подхват подтверждённых документов ЮЛ периметра (см.
`documents-api` «Подхват документов») и одно событие `audit_event(CONSOLIDATION_CREATE)`, у которого `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` — ровно эти ЮЛ после `legal_entity_ids` (поле отсутствует или `null`) — текущие члены ГСК; с `legal_entity_ids` — ровно эти ЮЛ после
схлопывания дублей (порядок первого вхождения), каждое SHALL принадлежать ГК консолидации схлопывания дублей (порядок первого вхождения), каждое SHALL принадлежать ГК консолидации
(`legal_entity.company_group_id`), иначе 422 `ENTITY_NOT_IN_GROUP` с перечнем `legal_entity_ids` чужих в порядке (`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`, история с одной записью, - **THEN** 201; версия №1 `INITIATED`, `reporting_date = 2024-10-01`, два `consolidation_member`, история с одной записью,
`audit_event(CONSOLIDATION_CREATE)` с `consolidation_version_id`; активной блокировки нет `audit_event(CONSOLIDATION_CREATE)` с `consolidation_version_id`; активной блокировки нет
#### Scenario: Правила действуют на дату создания версии
- **WHEN** в день создания консолидации за 2024 год действует версия правил 2, а версия 1 действовала в отчётном периоде
- **THEN** `consolidation_version.elimination_rules_version = 2`; номер не меняется при последующей публикации правил
#### Scenario: Выбранный периметр шире ГСК #### Scenario: Выбранный периметр шире ГСК
- **WHEN** в ГК три ЮЛ, в ГСК два из них, создаётся консолидация с `legal_entity_ids` из всех трёх ЮЛ ГК - **WHEN** в ГК три ЮЛ, в ГСК два из них, создаётся консолидация с `legal_entity_ids` из всех трёх ЮЛ ГК
- **THEN** 201; в `members` три ЮЛ; `after_value.members` события `CONSOLIDATION_CREATE` — три ЮЛ; состав ГСК не изменился - **THEN** 201; в `members` три ЮЛ; `after_value.members` события `CONSOLIDATION_CREATE` — три ЮЛ; состав ГСК не изменился