From a6418ca475c90764608f244c8dd7e94b91769af8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 12 Oct 2026 19:42:31 +0400 Subject: [PATCH] =?UTF-8?q?fix(consolidation):=20=D1=80=D0=B5=D0=B6=D0=B8?= =?UTF-8?q?=D0=BC=20=C2=AB=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=C2=BB=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=BC=D0=B5=D0=BD=D1=8F=D0=B5=D1=82=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D0=BE=D0=B4=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?= =?UTF-8?q?=20=D1=83=20=D0=9E=D0=A1=D0=92=20=C2=AB=D0=BD=D0=B5=20=D1=82?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=C2=BB=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Квартальные ОСВ, загруженные в консолидацию своего периода и уже привязанные, в режиме становились «нарастающими за квартал версии»: select_sources возвращал все такие документы, и свод брал их строки как есть — консолидация РБ 2024 Q3 на стенде задвоилась бы. Теперь подмена — только если период шапки версии не подходит (fits_by_header); регрессионный тест test_mode_keeps_header_period_when_it_fits_version. Co-Authored-By: Claude Opus 5.5 --- README.md | 9 +++-- api/src/domain/version_period.py | 26 +++++++++---- api/src/services/consolidation_documents.py | 15 ++++++-- api/tests/test_domain_version_period.py | 22 +++++++---- api/tests/test_version_period.py | 41 +++++++++++++++++++++ 5 files changed, 89 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 144596d..c21a76d 100644 --- a/README.md +++ b/README.md @@ -714,10 +714,11 @@ Q2–Q4 не подходят (R-14a). Привязываются только ` - `period_check` документа остаётся `UNKNOWN` — подходящесть — функция (документ, версия), а не свойство документа (OQ-120). - **Временный режим «период ОСВ из версии»** (`CONSOLIDATION_PERIOD_FROM_VERSION=true`, в контуре — `OPENBAO__APP__CONSOLIDATION_PERIOD_FROM_VERSION`; по умолчанию выключен; arm-core#103, OQ-161): ОСВ, загруженная в - консолидацию (`stored_file.consolidation_id`), считается нарастающей за год и квартал консолидации, какой бы период ни - стоял в шапке, — для привязки, свода, чек-листа и колонки «ОСВ» периметра (`src/domain/version_period.py`). Документ - не меняется (карточка показывает период из шапки); склейки кварталов нет — на счёт ЮЛ один слот, побеждает - подтверждённая последней; ББ/ОФР и файлы с рабочего стола режим не затрагивает. Только для тестов на произвольных ОСВ. + консолидацию (`stored_file.consolidation_id`), период шапки которой версии **не подходит**, считается нарастающей за год + и квартал консолидации — для привязки, свода, чек-листа и колонки «ОСВ» периметра (`src/domain/version_period.py`). + ОСВ, подходящие по шапке, не трогаются (квартальные склеиваются как раньше). Документ не меняется (карточка показывает + период из шапки); у подменённых ОСВ склейки нет — на счёт ЮЛ один слот, побеждает подтверждённая последней; ББ/ОФР и + файлы с рабочего стола режим не затрагивает. Только для тестов на произвольных ОСВ. ### Чек-лист diff --git a/api/src/domain/version_period.py b/api/src/domain/version_period.py index 63619fd..d047f2e 100644 --- a/api/src/domain/version_period.py +++ b/api/src/domain/version_period.py @@ -1,12 +1,15 @@ """ВРЕМЕННЫЙ режим «период ОСВ из версии консолидации» (arm-core#103, настройка `CONSOLIDATION_PERIOD_FROM_VERSION`). -Для тестов на произвольных ОСВ: документ ОСВ, загруженный в консолидацию (`stored_file.consolidation_id`), считается -нарастающим за год и квартал этой консолидации, какой бы период ни стоял в его шапке. Первичка не мутируется: -подменяется только представление документа (`VersionPeriodDocument`) при привязке к версии (`fits`, ключ замены R-1), -сборке свода (`select_sources`), чек-листе и колонке «ОСВ» периметра; карточка документа показывает период из шапки. +Для тестов на произвольных ОСВ: документ ОСВ, загруженный в консолидацию (`stored_file.consolidation_id`), период шапки +которого версии НЕ подходит, считается нарастающим за год и квартал этой консолидации. Документ, подходящий версии по +шапке, остаётся как есть — квартальные ОСВ по-прежнему склеиваются, уже привязанные не задваиваются. Первичка не +мутируется: подменяется только представление документа (`VersionPeriodDocument`) при привязке к версии (`fits`, ключ +замены R-1), сборке свода (`select_sources`), чек-листе и колонке «ОСВ» периметра; карточка документа показывает период +из шапки. -Следствие: склейки кварталов нет — все ОСВ одного счёта одного ЮЛ в консолидации занимают один слот привязки, побеждает -подтверждённая последней. ББ/ОФР и файлы без консолидации (рабочий стол) режим не затрагивает. +Следствие: подменённые ОСВ одного счёта одного ЮЛ занимают один слот привязки (нарастающий за квартал версии), побеждает +подтверждённая последней, и склейки кварталов у них нет. ББ/ОФР и файлы без консолидации (рабочий стол) режим не +затрагивает. ASSUMPTION(OQ-161): правила в `05-domain-rules.md` нет — режим временный, по решению владельца 12.10.2026; по умолчанию выключен, без настройки поведение §2 / D-5 Среза 4 не меняется. @@ -64,9 +67,16 @@ def takes_version_period( document_type: DocumentType, file_consolidation_id: uuid.UUID | None, consolidation_id: uuid.UUID, + fits_by_header: bool, ) -> bool: - """Берёт ли документ период версии: режим включён, документ — ОСВ, его файл загружен в эту консолидацию.""" - return enabled and document_type == DocumentType.OSV and file_consolidation_id == consolidation_id + """Берёт ли документ период версии: режим включён, документ — ОСВ, его файл загружен в эту консолидацию, а период + шапки версии не подходит (`fits_by_header` — период шапки подходит версии, как в `fits` без проверки периметра).""" + return ( + enabled + and document_type == DocumentType.OSV + and file_consolidation_id == consolidation_id + and not fits_by_header + ) def with_version_period(document: DocumentFields, *, year: int, quarter: PeriodQuarter) -> VersionPeriodDocument: diff --git a/api/src/services/consolidation_documents.py b/api/src/services/consolidation_documents.py index a2b9924..13d4b02 100644 --- a/api/src/services/consolidation_documents.py +++ b/api/src/services/consolidation_documents.py @@ -71,9 +71,9 @@ def period_from_version() -> bool: async def for_consolidation( session: AsyncSession, documents: Sequence[Document], consolidation: Consolidation ) -> list[DocumentFields]: - """Документы так, как их видит консолидация: при режиме `period_from_version` ОСВ, загруженные в неё, — с периодом - версии (`with_version_period`), прочие — как есть. Режим выключен — `documents` без запросов к БД; включён — один - запрос `consolidation_id` файлов.""" + """Документы так, как их видит консолидация: при режиме `period_from_version` ОСВ, загруженные в неё и не подходящие + версии по периоду шапки, — с периодом версии (`with_version_period`), прочие — как есть. Режим выключен — + `documents` без запросов к БД; включён — один запрос `consolidation_id` файлов.""" if not period_from_version() or not documents: return list(documents) file_ids = {document.stored_file_id for document in documents} @@ -85,6 +85,7 @@ async def for_consolidation( document_type=document.document_type, file_consolidation_id=file_consolidations.get(document.stored_file_id), consolidation_id=consolidation.id, + fits_by_header=_period_fits(document, consolidation), ) else document for document in documents @@ -128,7 +129,13 @@ def fits( (R-14; ББ/ОФР за `Q1` — только версии `Q1`, R-14a). Статус документа и файла не проверяется — это дело вызывающего (привязываются только `CONFIRMED` незаменённых файлов). Период — у переданного представления: в режиме «период из версии» вызывающий передаёт `for_consolidation(...)`. Чистая функция, без БД.""" - if document.legal_entity_id not in member_ids or document.period_year != consolidation.year: + return document.legal_entity_id in member_ids and _period_fits(document, consolidation) + + +def _period_fits(document: DocumentFields, consolidation: Consolidation) -> bool: + """Периодная часть `fits` (D-5, R-14, R-14a): год версии, квартал определён, нарастающий — за квартал версии, + квартальный — не позже него.""" + if document.period_year != consolidation.year: return False if document.period_quarter is None: return False diff --git a/api/tests/test_domain_version_period.py b/api/tests/test_domain_version_period.py index 4ed3e34..6400156 100644 --- a/api/tests/test_domain_version_period.py +++ b/api/tests/test_domain_version_period.py @@ -17,18 +17,23 @@ MEMBER = uuid.uuid4() @pytest.mark.parametrize( - ("enabled", "document_type", "file_consolidation_id", "expected"), + ("enabled", "document_type", "file_consolidation_id", "fits_by_header", "expected"), [ - (True, DocumentType.OSV, CONSOLIDATION, True), # ОСВ загружена в эту консолидацию - (False, DocumentType.OSV, CONSOLIDATION, False), # режим выключен — период из шапки - (True, DocumentType.OSV, OTHER, False), # загружена в другую консолидацию - (True, DocumentType.OSV, None, False), # загружена с рабочего стола, без консолидации - (True, DocumentType.BALANCE, CONSOLIDATION, False), # ББ/ОФР — только период из шапки - (True, DocumentType.INCOME, CONSOLIDATION, False), + (True, DocumentType.OSV, CONSOLIDATION, False, True), # ОСВ «не того» периода загружена в эту консолидацию + (True, DocumentType.OSV, CONSOLIDATION, True, False), # период шапки и так подходит — склейка кварталов цела + (False, DocumentType.OSV, CONSOLIDATION, False, False), # режим выключен — период из шапки + (True, DocumentType.OSV, OTHER, False, False), # загружена в другую консолидацию + (True, DocumentType.OSV, None, False, False), # загружена с рабочего стола, без консолидации + (True, DocumentType.BALANCE, CONSOLIDATION, False, False), # ББ/ОФР — только период из шапки + (True, DocumentType.INCOME, CONSOLIDATION, False, False), ], ) def test_takes_version_period( - enabled: bool, document_type: DocumentType, file_consolidation_id: uuid.UUID | None, expected: bool + enabled: bool, + document_type: DocumentType, + file_consolidation_id: uuid.UUID | None, + fits_by_header: bool, + expected: bool, ) -> None: assert ( takes_version_period( @@ -36,6 +41,7 @@ def test_takes_version_period( document_type=document_type, file_consolidation_id=file_consolidation_id, consolidation_id=CONSOLIDATION, + fits_by_header=fits_by_header, ) is expected ) diff --git a/api/tests/test_version_period.py b/api/tests/test_version_period.py index 6d8f291..44cc1b3 100644 --- a/api/tests/test_version_period.py +++ b/api/tests/test_version_period.py @@ -194,3 +194,44 @@ async def test_perimeter_pickup_binds_one_osv_per_account_latest_confirmed(seede 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"