mapping: новый файл без контекста наследует consolidation_id заменённого (#96)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
1e888d4670
commit
25307edee8
@ -660,8 +660,10 @@ async def supersede_previous( # noqa: PLR0913, PLR0915 - автозамена
|
||||
) -> list[StoredFile]:
|
||||
"""D-10, spec primary-files «Автозамена прежних версий»: файлы того же ЮЛ (не `SUPERSEDED`, не сам новый) с
|
||||
документом тех же `document_type`, `osv_account_code`, `period_year`, `period_quarter` → `SUPERSEDED`,
|
||||
`superseded_by_file_id = new_file.id`; у нового файла `version_no` = максимум заменённых + 1. На каждый
|
||||
заменённый файл — одно системное событие `FILE_REPLACE` (`user_id = NULL`, инициатор загрузки — в `description`,
|
||||
`superseded_by_file_id = new_file.id`; у нового файла `version_no` = максимум заменённых + 1; файл без
|
||||
консолидации (`consolidation_id` пуст) наследует её у первого по `id` заменённого файла, где она задана (v5.18,
|
||||
§9.5 п. 3). На каждый заменённый файл — одно системное событие `FILE_REPLACE` (`user_id = NULL`, инициатор
|
||||
загрузки — в `description`,
|
||||
`confirmed=true`, если среди заменённых документов есть `CONFIRMED`; решение владельца 05.10.2026). Документы и
|
||||
строки заменённых файлов не трогаются (история). Возвращает заменённые файлы; в транзакции вызывающего, без commit.
|
||||
|
||||
@ -729,6 +731,10 @@ async def supersede_previous( # noqa: PLR0913, PLR0915 - автозамена
|
||||
.execution_options(synchronize_session=False)
|
||||
)
|
||||
new_file.version_no = max(old.version_no for old in locked) + 1
|
||||
if new_file.consolidation_id is None: # v5.18, §9.5 п. 3: файл без контекста наследует консоль заменённого
|
||||
new_file.consolidation_id = next(
|
||||
(old.consolidation_id for old in locked if old.consolidation_id is not None), None
|
||||
)
|
||||
new_file.updated_at = now
|
||||
await session.flush()
|
||||
# привязки к версиям «в работе» снимаются у ВСЕХ документов заменённого файла (spec primary-files), а не только у
|
||||
|
||||
@ -20,17 +20,23 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from loguru import logger
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from src.contract import ack_msg_id, result_msg_id
|
||||
from src.contract.result import RecognitionResult
|
||||
from src.core.config import Settings, get_settings
|
||||
from src.domain.consolidation_periods import reporting_date
|
||||
from src.models.access import AppUser
|
||||
from src.models.accounting import ChartOfAccount
|
||||
from src.models.audit_ex import AuditEvent
|
||||
from src.models.consolidation import ConsolidationDocument, ConsolidationStatusHistory, ConsolidationVersion
|
||||
from src.models.consolidation import (
|
||||
Consolidation,
|
||||
ConsolidationDocument,
|
||||
ConsolidationStatusHistory,
|
||||
ConsolidationVersion,
|
||||
)
|
||||
from src.models.directories import LegalEntity
|
||||
from src.models.enums import (
|
||||
AuditAction,
|
||||
@ -1044,6 +1050,81 @@ async def test_version_chain(committed_sessionmaker: async_sessionmaker[AsyncSes
|
||||
assert event.after_value == {"file_id": str(v3_id), "version_no": 3, "unbound_version_ids": []}
|
||||
|
||||
|
||||
async def _consolidation_id(
|
||||
maker: async_sessionmaker[AsyncSession], entity_id: uuid.UUID, quarter: PeriodQuarter
|
||||
) -> uuid.UUID:
|
||||
"""Консолидация ГК этого ЮЛ за 2025/quarter (без версий: для наследования они не нужны).
|
||||
|
||||
ЮЛ без ГК получает ГК (консолидация требует группу) и привязывается к ней."""
|
||||
async with maker() as session, session.begin():
|
||||
entity = await session.get(LegalEntity, entity_id)
|
||||
assert entity is not None
|
||||
if entity.company_group_id is None:
|
||||
group = await make_company_group(session, name="ГК Наследование")
|
||||
entity.company_group_id = group.id
|
||||
consolidation = Consolidation(
|
||||
company_group_id=entity.company_group_id,
|
||||
year=2025,
|
||||
quarter=quarter,
|
||||
reporting_date=reporting_date(2025, quarter),
|
||||
created_at=_now(),
|
||||
)
|
||||
session.add(consolidation)
|
||||
await session.flush()
|
||||
return consolidation.id
|
||||
|
||||
|
||||
async def _set_consolidation(
|
||||
maker: async_sessionmaker[AsyncSession], file_id: uuid.UUID, consolidation_id: uuid.UUID | None
|
||||
) -> None:
|
||||
async with maker() as session, session.begin():
|
||||
await session.execute(
|
||||
update(StoredFile).where(StoredFile.id == file_id).values(consolidation_id=consolidation_id)
|
||||
)
|
||||
|
||||
|
||||
async def test_supersede_inherits_consolidation_from_first_with_value(
|
||||
committed_sessionmaker: async_sessionmaker[AsyncSession],
|
||||
) -> None:
|
||||
"""Спека «Новый файл без контекста наследует консолидацию» + Review Focus 5: два заменяемых файла, у первого по `id`
|
||||
`consolidation_id` пуст, у второго задан — новый получает значение второго."""
|
||||
maker, storage = committed_sessionmaker, FakeStorage()
|
||||
entity_id = await _entity(maker)
|
||||
c1 = await _consolidation_id(maker, entity_id, PeriodQuarter.Q1)
|
||||
first_id = await _old_file(maker, entity_id, [OSV_60_2025_Q1])
|
||||
second_id = await _old_file(maker, entity_id, [OSV_60_2025_Q1])
|
||||
empty_id, filled_id = sorted([first_id, second_id])
|
||||
await _set_consolidation(maker, filled_id, c1)
|
||||
async with maker() as session:
|
||||
entity = await session.get(LegalEntity, entity_id)
|
||||
new_id, _, _ = await _prepare(maker, storage, _minimal(), entity=entity)
|
||||
|
||||
assert await process_once(maker, storage, _settings()) is True
|
||||
|
||||
assert (await _file(maker, new_id)).consolidation_id == c1
|
||||
assert (await _file(maker, empty_id)).status is FileStatus.SUPERSEDED
|
||||
assert (await _file(maker, filled_id)).status is FileStatus.SUPERSEDED
|
||||
|
||||
|
||||
async def test_supersede_keeps_own_consolidation(committed_sessionmaker: async_sessionmaker[AsyncSession]) -> None:
|
||||
"""Спека «Новый файл с контекстом сохраняет свой»: старый в C1, новый загружен в C2 — остаётся C2."""
|
||||
maker, storage = committed_sessionmaker, FakeStorage()
|
||||
entity_id = await _entity(maker)
|
||||
c1 = await _consolidation_id(maker, entity_id, PeriodQuarter.Q1)
|
||||
c2 = await _consolidation_id(maker, entity_id, PeriodQuarter.Q2)
|
||||
old_id = await _old_file(maker, entity_id, [OSV_60_2025_Q1])
|
||||
await _set_consolidation(maker, old_id, c1)
|
||||
async with maker() as session:
|
||||
entity = await session.get(LegalEntity, entity_id)
|
||||
new_id, _, _ = await _prepare(maker, storage, _minimal(), entity=entity)
|
||||
await _set_consolidation(maker, new_id, c2)
|
||||
|
||||
assert await process_once(maker, storage, _settings()) is True
|
||||
|
||||
assert (await _file(maker, new_id)).consolidation_id == c2
|
||||
assert (await _file(maker, old_id)).status is FileStatus.SUPERSEDED
|
||||
|
||||
|
||||
async def test_one_event_per_replaced_file(committed_sessionmaker: async_sessionmaker[AsyncSession]):
|
||||
"""Новый файл с ОСВ 60 и 62 заменяет прежний файл с теми же двумя документами одним событием `FILE_REPLACE`,
|
||||
в `description` — оба документа."""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user