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]:
|
) -> list[StoredFile]:
|
||||||
"""D-10, spec primary-files «Автозамена прежних версий»: файлы того же ЮЛ (не `SUPERSEDED`, не сам новый) с
|
"""D-10, spec primary-files «Автозамена прежних версий»: файлы того же ЮЛ (не `SUPERSEDED`, не сам новый) с
|
||||||
документом тех же `document_type`, `osv_account_code`, `period_year`, `period_quarter` → `SUPERSEDED`,
|
документом тех же `document_type`, `osv_account_code`, `period_year`, `period_quarter` → `SUPERSEDED`,
|
||||||
`superseded_by_file_id = new_file.id`; у нового файла `version_no` = максимум заменённых + 1. На каждый
|
`superseded_by_file_id = new_file.id`; у нового файла `version_no` = максимум заменённых + 1; файл без
|
||||||
заменённый файл — одно системное событие `FILE_REPLACE` (`user_id = NULL`, инициатор загрузки — в `description`,
|
консолидации (`consolidation_id` пуст) наследует её у первого по `id` заменённого файла, где она задана (v5.18,
|
||||||
|
§9.5 п. 3). На каждый заменённый файл — одно системное событие `FILE_REPLACE` (`user_id = NULL`, инициатор
|
||||||
|
загрузки — в `description`,
|
||||||
`confirmed=true`, если среди заменённых документов есть `CONFIRMED`; решение владельца 05.10.2026). Документы и
|
`confirmed=true`, если среди заменённых документов есть `CONFIRMED`; решение владельца 05.10.2026). Документы и
|
||||||
строки заменённых файлов не трогаются (история). Возвращает заменённые файлы; в транзакции вызывающего, без commit.
|
строки заменённых файлов не трогаются (история). Возвращает заменённые файлы; в транзакции вызывающего, без commit.
|
||||||
|
|
||||||
@ -729,6 +731,10 @@ async def supersede_previous( # noqa: PLR0913, PLR0915 - автозамена
|
|||||||
.execution_options(synchronize_session=False)
|
.execution_options(synchronize_session=False)
|
||||||
)
|
)
|
||||||
new_file.version_no = max(old.version_no for old in locked) + 1
|
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
|
new_file.updated_at = now
|
||||||
await session.flush()
|
await session.flush()
|
||||||
# привязки к версиям «в работе» снимаются у ВСЕХ документов заменённого файла (spec primary-files), а не только у
|
# привязки к версиям «в работе» снимаются у ВСЕХ документов заменённого файла (spec primary-files), а не только у
|
||||||
|
|||||||
@ -20,17 +20,23 @@ from typing import Any
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select, update
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
|
|
||||||
from src.contract import ack_msg_id, result_msg_id
|
from src.contract import ack_msg_id, result_msg_id
|
||||||
from src.contract.result import RecognitionResult
|
from src.contract.result import RecognitionResult
|
||||||
from src.core.config import Settings, get_settings
|
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.access import AppUser
|
||||||
from src.models.accounting import ChartOfAccount
|
from src.models.accounting import ChartOfAccount
|
||||||
from src.models.audit_ex import AuditEvent
|
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.directories import LegalEntity
|
||||||
from src.models.enums import (
|
from src.models.enums import (
|
||||||
AuditAction,
|
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": []}
|
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]):
|
async def test_one_event_per_replaced_file(committed_sessionmaker: async_sessionmaker[AsyncSession]):
|
||||||
"""Новый файл с ОСВ 60 и 62 заменяет прежний файл с теми же двумя документами одним событием `FILE_REPLACE`,
|
"""Новый файл с ОСВ 60 и 62 заменяет прежний файл с теми же двумя документами одним событием `FILE_REPLACE`,
|
||||||
в `description` — оба документа."""
|
в `description` — оба документа."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user