Квартальные ОСВ, загруженные в консолидацию своего периода и уже привязанные, в режиме становились «нарастающими за квартал версии»: 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>
238 lines
11 KiB
Python
238 lines
11 KiB
Python
"""Временный режим «период ОСВ из версии консолидации» (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"
|