"""Временный режим «период ОСВ из версии консолидации» (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"