arm-core/api/tests/test_version_period.py
Andrey Epifancev a6418ca475 fix(consolidation): режим «период из версии» подменяет период только у ОСВ «не того» периода (#103)
Квартальные ОСВ, загруженные в консолидацию своего периода и уже привязанные, в режиме
становились «нарастающими за квартал версии»: select_sources возвращал все такие документы,
и свод брал их строки как есть — консолидация РБ 2024 Q3 на стенде задвоилась бы.
Теперь подмена — только если период шапки версии не подходит (fits_by_header);
регрессионный тест test_mode_keeps_header_period_when_it_fits_version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:42:31 +04:00

238 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Временный режим «период ОСВ из версии консолидации» (arm-core#103, `CONSOLIDATION_PERIOD_FROM_VERSION`).
ОСВ, загруженная в консолидацию (`stored_file.consolidation_id`), при включённом режиме считается нарастающей за год и
квартал консолидации: привязывается при подтверждении и подхвате, попадает в свод, чек-лист и колонку «ОСВ» периметра.
Выключенный режим, файлы вне консолидации и ББ/ОФР — период из шапки, как раньше.
"""
import datetime
import uuid
from collections.abc import Sequence
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.config import settings
from src.models.access import AppUser
from src.models.consolidation import (
Consolidation,
ConsolidationDocument,
ConsolidationMember,
ConsolidationVersion,
)
from src.models.directories import LegalEntity, RegionalOffice
from src.models.enums import DocumentStatus, PeriodQuarter
from src.models.files import Document, StoredFile
from src.services import consolidation_documents
from tests.conftest import AuthClientFactory
from tests.factories import make_company_group, make_confirmed_osv, make_consolidation, make_legal_entity
HTTP_OK = 200
KA_RF = "ka_rf@armka.local"
ROSTOV = "Ростовский региональный филиал"
VERSION_YEAR = 2026 # консолидация 2026 Q1
HEADER_YEAR = 2025 # а ОСВ в шапке — за 1 кв. 2025
ACCOUNT = "60"
ROWS = [{"name": "ООО Контрагент", "inn": "7701234567", "closing_debit": 10}]
Q1, Q2 = PeriodQuarter.Q1, PeriodQuarter.Q2
@pytest.fixture
def period_from_version(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(settings, "CONSOLIDATION_PERIOD_FROM_VERSION", True)
async def _world(
session: AsyncSession, members: int = 2
) -> tuple[Consolidation, ConsolidationVersion, list[LegalEntity]]:
office = (await session.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
group = await make_company_group(session, name=f"ГК {uuid.uuid4().hex[:6]}", office=office)
entities = [await make_legal_entity(session, name=f"ООО Член {n}", group=group) for n in range(members + 1)]
user = (await session.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
consolidation, version = await make_consolidation(
session, group=group, year=VERSION_YEAR, quarter=Q1, members=entities[:members], initiator=user
)
return consolidation, version, entities
async def _osv( # noqa: PLR0913 - тестовая фабрика: ЮЛ, консолидация файла, период, статус, время подтверждения
session: AsyncSession,
entity: LegalEntity,
consolidation: Consolidation | None,
*,
quarter: PeriodQuarter = Q1,
cumulative: bool = True,
status: DocumentStatus = DocumentStatus.NEEDS_CHECK,
confirmed_at: datetime.datetime | None = None,
) -> Document:
"""ОСВ 60 за `HEADER_YEAR` со строкой контрагента; файл загружен в `consolidation` (`None` — рабочий стол)."""
document = await make_confirmed_osv(
session, entity=entity, account=ACCOUNT, year=HEADER_YEAR, quarter=quarter, is_cumulative=cumulative, rows=ROWS
)
file = await session.get(StoredFile, document.stored_file_id)
assert file is not None
file.consolidation_id = consolidation.id if consolidation is not None else None
document.status = status
document.confirmed_at = confirmed_at if status is DocumentStatus.CONFIRMED else None
await session.flush()
return document
async def _bound(session: AsyncSession, version_id: uuid.UUID) -> set[uuid.UUID]:
stmt = select(ConsolidationDocument.document_id).where(ConsolidationDocument.consolidation_version_id == version_id)
return set((await session.execute(stmt)).scalars())
async def _confirm(auth_client: AuthClientFactory, documents: Sequence[Document]) -> None:
client = await auth_client(KA_RF)
for document in documents:
response = await client.post(f"/documents/{document.id}/confirm")
assert response.status_code == HTTP_OK, response.text
@pytest.mark.integration
@pytest.mark.usefixtures("period_from_version")
async def test_confirmed_osv_of_other_period_goes_to_version_and_summary(
seeded: AsyncSession, auth_client: AuthClientFactory
) -> None:
"""Сценарий владельца 12.10.2026: ОСВ за 1 кв. 2025 в консолидации 2026 Q1 после подтверждения — в своде."""
consolidation, version, (first, second, _) = await _world(seeded)
documents = [await _osv(seeded, entity, consolidation) for entity in (first, second)]
await _confirm(auth_client, documents)
assert await _bound(seeded, version.id) == {document.id for document in documents}
client = await auth_client(KA_RF)
summary = await client.get(f"/consolidations/versions/{version.id}/osv?limit=100")
assert summary.status_code == HTTP_OK, summary.text
assert summary.json()["total"] == len(documents) * len(ROWS)
# первичка не мутируется: период документа — из шапки
document = await seeded.get(Document, documents[0].id, populate_existing=True)
assert document is not None
assert (document.period_year, document.period_quarter) == (HEADER_YEAR, Q1)
@pytest.mark.integration
async def test_without_mode_osv_of_other_period_stays_unbound(
seeded: AsyncSession, auth_client: AuthClientFactory
) -> None:
consolidation, version, (first, _, _) = await _world(seeded)
document = await _osv(seeded, first, consolidation)
await _confirm(auth_client, [document])
assert await _bound(seeded, version.id) == set()
@pytest.mark.integration
@pytest.mark.usefixtures("period_from_version")
async def test_mode_ignores_osv_uploaded_outside_consolidation(
seeded: AsyncSession, auth_client: AuthClientFactory
) -> None:
"""Файл с рабочего стола (`consolidation_id` пуст) — период из шапки: 2025 не подходит версии 2026."""
_, version, (first, _, _) = await _world(seeded)
document = await _osv(seeded, first, None)
await _confirm(auth_client, [document])
assert await _bound(seeded, version.id) == set()
@pytest.mark.integration
@pytest.mark.usefixtures("period_from_version")
async def test_checklist_and_card_see_unconfirmed_osv_of_other_period(
seeded: AsyncSession, auth_client: AuthClientFactory
) -> None:
consolidation, version, (first, _, _) = await _world(seeded)
await _osv(seeded, first, consolidation)
client = await auth_client(KA_RF)
checklist = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json()
card = (await client.get(f"/consolidations/versions/{version.id}")).json()
rules = [(rule["document_type"], rule["account"]) for rule in checklist["rules"]]
row = next(row for row in checklist["rows"] if row["legal_entity_id"] == str(first.id))
assert row["cells"][rules.index(("OSV", ACCOUNT))]["status"] == "NEEDS_CHECK"
member = next(member for member in card["members"] if member["id"] == str(first.id))
assert member["osv_accounts"] == [ACCOUNT]
@pytest.mark.integration
@pytest.mark.usefixtures("period_from_version")
async def test_perimeter_pickup_binds_one_osv_per_account_latest_confirmed(seeded: AsyncSession) -> None:
"""Подхват (`bind_confirmed`): все ОСВ счёта одного ЮЛ в консолидации — один слот, побеждает поздняя."""
consolidation, version, (_, _, newcomer) = await _world(seeded)
earlier = datetime.datetime(2026, 10, 12, 9, tzinfo=datetime.UTC)
later = earlier + datetime.timedelta(hours=1)
await _osv(
seeded,
newcomer,
consolidation,
quarter=Q1,
cumulative=False,
status=DocumentStatus.CONFIRMED,
confirmed_at=earlier,
)
latest = await _osv(
seeded,
newcomer,
consolidation,
quarter=Q2,
cumulative=False,
status=DocumentStatus.CONFIRMED,
confirmed_at=later,
)
seeded.add(
ConsolidationMember(consolidation_version_id=version.id, legal_entity_id=newcomer.id, is_head_entity=False)
)
await seeded.flush()
bound = await consolidation_documents.bind_confirmed(
seeded, version=version, consolidation=consolidation, entity_ids=[newcomer.id]
)
assert bound == 1
assert await _bound(seeded, version.id) == {latest.id}
@pytest.mark.integration
async def test_mode_keeps_header_period_when_it_fits_version(
seeded: AsyncSession, auth_client: AuthClientFactory, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Регрессия: квартальные ОСВ Q1–Q3, загруженные в консолидацию Q3 и привязанные до включения режима, в режиме
по-прежнему склеиваются в одну строку свода (их период из шапки подходит версии — подмены нет), а не идут «как есть»
трижды как нарастающие за Q3."""
office = (await seeded.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
group = await make_company_group(seeded, name=f"ГК {uuid.uuid4().hex[:6]}", office=office)
entities = [await make_legal_entity(seeded, name=f"ООО Член {n}", group=group) for n in range(2)]
user = (await seeded.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
consolidation, version = await make_consolidation(
seeded, group=group, year=HEADER_YEAR, quarter=PeriodQuarter.Q3, members=entities, initiator=user
)
quarters = (Q1, Q2, PeriodQuarter.Q3)
for quarter in quarters:
document = await make_confirmed_osv(
seeded,
entity=entities[0],
account=ACCOUNT,
year=HEADER_YEAR,
quarter=quarter,
is_cumulative=False,
rows=[{"name": "ООО Контрагент", "inn": "7701234567", "turnover_debit": 10}],
)
file = await seeded.get(StoredFile, document.stored_file_id)
assert file is not None
file.consolidation_id = consolidation.id
await seeded.flush()
assert await consolidation_documents.bind_confirmed(
seeded, version=version, consolidation=consolidation, entity_ids=[entities[0].id]
) == len(quarters) # режим выключен: три квартальных источника
monkeypatch.setattr(settings, "CONSOLIDATION_PERIOD_FROM_VERSION", True)
client = await auth_client(KA_RF)
body = (await client.get(f"/consolidations/versions/{version.id}/osv?limit=100")).json()
assert body["total"] == 1, body
assert body["items"][0]["turnover_debit"] == "30.00"