fix(consolidation): равнозначность Q1 — только для ОСВ, ББ/ОФР за Q1 к версиям Q2–Q4 не привязываются (R-14a)
ББ/ОФР за Q1 — отчёт на конец квартала, не источник версий Q2–Q4. В `consolidation_documents._cumulative` равнозначность Q1 (`effective_cumulative`) применяется только к `document_type = OSV`; для ББ/ОФР — прежнее правило D-5 (NULL — нарастающий, только квартал версии), ключ замены не меняется. - тесты: ББ/ОФР за Q1 подходят только версии Q1; ключ замены ББ/ОФР всегда с признаком нарастающего, ОСВ за Q1 — без различия флага; подтверждение ББ/ОФР за Q1 при версии Q3 — без привязки и перехода; ячейка ББ чек-листа Q3 не закрывается ББ Q1 - спека documents-api: ключ замены R-1 с признаком нарастающего (вместо устаревшей «тройки»), равнозначность Q1 — только для ОСВ; consolidated-osv — «ОСВ за Q1» - design #81: R-14 + R-14a Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
d3c4964e92
commit
0bfe9eea5f
@ -8,9 +8,10 @@
|
||||
(`unbind_entity`). Версии вне «в работе» не трогаются.
|
||||
|
||||
Ключ замены (R-1): у одного ЮЛ в одной версии на ключ `(document_type, osv_account_code, period_quarter, cumulative)`
|
||||
привязан один документ — с самым поздним `confirmed_at`; `cumulative = effective_cumulative(...)` (NULL — ББ/ОФР —
|
||||
нарастающий, решение №10). Квартальный и нарастающий документы одного квартала друг друга не вытесняют — кроме `Q1`:
|
||||
документ за `Q1` равнозначен обоим видам (R-14), нарастающий и квартальный `Q1` — один слот, побеждает поздний.
|
||||
привязан один документ — с самым поздним `confirmed_at`; `cumulative = _cumulative(document)` (NULL — ББ/ОФР —
|
||||
нарастающий, решение №10). Квартальный и нарастающий документы одного квартала друг друга не вытесняют — кроме ОСВ за
|
||||
`Q1`: она равнозначна обоим видам (R-14), нарастающая и квартальная `Q1` — один слот, побеждает поздняя; ББ/ОФР
|
||||
равнозначность не касается (R-14a).
|
||||
|
||||
Порядок блокировок (D-9, R-2, R-3): файл → документ (`FOR NO KEY UPDATE`) → advisory ГК → версии `FOR UPDATE` по `id`
|
||||
(`repo.lock_versions_for_entity`); автозамена — advisory ЮЛ → старые файлы → advisory ГК → версии; создание версии —
|
||||
@ -28,7 +29,7 @@ from sqlalchemy import delete, select
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.consolidation_periods import QUARTER_ORDER, effective_cumulative
|
||||
from src.domain.consolidation_periods import QUARTER_ORDER, counts_as_cumulative, effective_cumulative
|
||||
from src.domain.policy import IN_WORK_STATUSES
|
||||
from src.models.consolidation import Consolidation, ConsolidationDocument, ConsolidationMember, ConsolidationVersion
|
||||
from src.models.enums import ConsolidationStatus, DocumentStatus, DocumentType, FileStatus, PeriodQuarter
|
||||
@ -57,9 +58,12 @@ def _rowcount(result: Any) -> int:
|
||||
|
||||
|
||||
def _cumulative(document: Document) -> bool:
|
||||
"""Нарастающий ли документ для привязки (R-1, решение №10, R-14): `consolidation_periods.effective_cumulative` —
|
||||
NULL нарастающий; документ за `Q1` — квартальный при любом флаге."""
|
||||
return effective_cumulative(document.is_cumulative, document.period_quarter)
|
||||
"""Нарастающий ли документ для привязки и ключа замены (R-1, решение №10, R-14, R-14a): NULL — нарастающий;
|
||||
ОСВ за `Q1` — квартальная при любом флаге (`effective_cumulative`). Равнозначность `Q1` — только для ОСВ: ББ/ОФР
|
||||
за `Q1` — отчёт на конец квартала, не источник версий `Q2`–`Q4` (`counts_as_cumulative`, всегда нарастающие)."""
|
||||
if document.document_type == DocumentType.OSV:
|
||||
return effective_cumulative(document.is_cumulative, document.period_quarter)
|
||||
return counts_as_cumulative(document.is_cumulative)
|
||||
|
||||
|
||||
def _key(document: Document) -> BindingKey:
|
||||
@ -86,8 +90,8 @@ def fits(
|
||||
member_ids: Collection[uuid.UUID],
|
||||
) -> bool:
|
||||
"""Подходит ли документ версии (D-5): ЮЛ — член периметра, год версии, квартал определён и: нарастающий (или
|
||||
NULL) — квартал версии; квартальный — не позже квартала версии; документ за `Q1` — любой версии года при любом
|
||||
флаге (R-14). Статус документа и файла не проверяется — это
|
||||
NULL) — квартал версии; квартальный — не позже квартала версии; ОСВ за `Q1` — любой версии года при любом флаге
|
||||
(R-14; ББ/ОФР за `Q1` — только версии `Q1`, R-14a). Статус документа и файла не проверяется — это
|
||||
дело вызывающего (привязываются только `CONFIRMED` незаменённых файлов). Чистая функция, без БД."""
|
||||
if document.legal_entity_id not in member_ids or document.period_year != consolidation.year:
|
||||
return False
|
||||
|
||||
@ -76,12 +76,17 @@ _STRANGER = uuid.uuid4()
|
||||
|
||||
|
||||
def _pure_document(
|
||||
*, entity: uuid.UUID = _MEMBER, year: int = YEAR, quarter: PeriodQuarter | None = Q3, cumulative: bool | None
|
||||
*,
|
||||
entity: uuid.UUID = _MEMBER,
|
||||
year: int = YEAR,
|
||||
quarter: PeriodQuarter | None = Q3,
|
||||
cumulative: bool | None,
|
||||
kind: DocumentType = DocumentType.OSV,
|
||||
) -> Document:
|
||||
return Document(
|
||||
legal_entity_id=entity,
|
||||
document_type=DocumentType.OSV,
|
||||
osv_account_code=ACCOUNT,
|
||||
document_type=kind,
|
||||
osv_account_code=ACCOUNT if kind == DocumentType.OSV else None,
|
||||
period_year=year,
|
||||
period_quarter=quarter,
|
||||
is_cumulative=cumulative,
|
||||
@ -207,13 +212,43 @@ async def _bind(session: AsyncSession, version: ConsolidationVersion, document:
|
||||
@pytest.mark.parametrize("version_quarter", [Q1, Q2, Q3, Q4])
|
||||
@pytest.mark.parametrize("cumulative", [True, False, None])
|
||||
def test_q1_document_fits_every_version_of_year(version_quarter: PeriodQuarter, cumulative: bool | None) -> None:
|
||||
"""R-14: документ за Q1 равнозначен нарастающему и квартальному — подходит любой версии года."""
|
||||
"""R-14: ОСВ за Q1 равнозначна нарастающей и квартальной — подходит любой версии года."""
|
||||
version = ConsolidationVersion(status=ConsolidationStatus.INITIATED)
|
||||
consolidation = Consolidation(year=YEAR, quarter=version_quarter)
|
||||
|
||||
assert fits(_pure_document(cumulative=cumulative, quarter=Q1), version, consolidation, {_MEMBER}) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", [DocumentType.BALANCE, DocumentType.INCOME])
|
||||
@pytest.mark.parametrize(("version_quarter", "expected"), [(Q1, True), (Q2, False), (Q3, False), (Q4, False)])
|
||||
def test_balance_and_income_of_q1_fit_only_q1_version(
|
||||
kind: DocumentType, version_quarter: PeriodQuarter, expected: bool
|
||||
) -> None:
|
||||
"""R-14a: ББ/ОФР за Q1 — отчёт на конец Q1, не источник версий Q2–Q4 (NULL — нарастающий, D-5 без изменений)."""
|
||||
version = ConsolidationVersion(status=ConsolidationStatus.INITIATED)
|
||||
consolidation = Consolidation(year=YEAR, quarter=version_quarter)
|
||||
document = _pure_document(cumulative=None, quarter=Q1, kind=kind)
|
||||
|
||||
assert fits(document, version, consolidation, {_MEMBER}) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", [DocumentType.BALANCE, DocumentType.INCOME])
|
||||
@pytest.mark.parametrize("quarter", [Q1, Q2, Q3, Q4])
|
||||
def test_binding_key_of_balance_and_income_is_cumulative(kind: DocumentType, quarter: PeriodQuarter) -> None:
|
||||
"""R-14a: равнозначность Q1 ключ замены ББ/ОФР не меняет: признак нарастающего у них всегда `True` (№10)."""
|
||||
document = _pure_document(cumulative=None, quarter=quarter, kind=kind)
|
||||
|
||||
assert consolidation_documents._key(document)[-1] is True # noqa: SLF001 - ключ замены R-1 — внутренний
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cumulative", [True, False])
|
||||
def test_binding_key_of_osv_q1_ignores_flag(cumulative: bool) -> None:
|
||||
"""R-14: нарастающая и квартальная ОСВ за Q1 одной пары — один слот привязки."""
|
||||
document = _pure_document(cumulative=cumulative, quarter=Q1)
|
||||
|
||||
assert consolidation_documents._key(document)[-1] is False # noqa: SLF001 - ключ замены R-1 — внутренний
|
||||
|
||||
|
||||
# --- подтверждение ----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@ -284,6 +319,26 @@ async def test_confirm_binds_cumulative_q1_to_version_q3(seeded: AsyncSession, a
|
||||
assert await _status(seeded, version.id) is ConsolidationStatus.DATA_CORRECTION
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("kind", [DocumentType.BALANCE, DocumentType.INCOME])
|
||||
async def test_confirm_does_not_bind_balance_or_income_of_q1_to_version_q3(
|
||||
seeded: AsyncSession, auth_client: AuthClientFactory, kind: DocumentType
|
||||
) -> None:
|
||||
"""R-14a: ББ/ОФР за Q1 к версии Q3 не привязываются и статус версии не двигают."""
|
||||
group, entities = await _group(seeded)
|
||||
version = await _version(seeded, group, entities)
|
||||
document = await _document(seeded, entities[0], quarter=Q1)
|
||||
document.document_type, document.osv_account_code, document.is_cumulative = kind, None, None
|
||||
await seeded.flush()
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
response = await client.post(f"/documents/{document.id}/confirm")
|
||||
|
||||
assert response.status_code == HTTP_OK, response.text
|
||||
assert await _bound(seeded, version.id) == set()
|
||||
assert await _status(seeded, version.id) is ConsolidationStatus.INITIATED
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("bound_cumulative", [False, True])
|
||||
async def test_confirm_q1_replaces_bound_q1_of_other_flag(
|
||||
|
||||
@ -226,6 +226,15 @@ def test_balance_of_q2_at_version_q3_is_missing() -> None:
|
||||
assert _cell(BALANCE, entity, bound=[balance]) == Cell(CellStatus.MISSING)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cumulative", [None, True, False])
|
||||
def test_balance_of_q1_at_version_q3_is_missing(cumulative: bool | None) -> None:
|
||||
"""R-14a: равнозначность Q1 касается только ОСВ — ББ за Q1 ячейку ББ версии Q3 не закрывает."""
|
||||
entity = _entity()
|
||||
balance = _doc(entity, kind=DocumentType.BALANCE, account=None, quarter=Q1, cumulative=cumulative)
|
||||
|
||||
assert _cell(BALANCE, entity, bound=[balance]) == Cell(CellStatus.MISSING)
|
||||
|
||||
|
||||
def test_documents_of_other_entity_are_ignored() -> None:
|
||||
entity, stranger = _entity(), _entity()
|
||||
foreign = _doc(stranger, quarter=Q3, cumulative=True)
|
||||
|
||||
@ -265,4 +265,4 @@ legal_entity_name, account_code, missing_quarters[]}}`. Фильтры `GET …/
|
||||
| 78 | 2026-10-06 | R-13: параметр `entity_names` убран из `build_workbook` — сигнатура `build_workbook(c, rows) -> bytes`; сборка книги выполняется в `run_in_threadpool` (линейная по строкам, явный счётчик строк вместо `sheet.max_row`) | строки и пробелы уже несут имена ЮЛ, параметр не использовался (YAGNI); план предписывал параметр — Interfaces Task 12 исправлены | если фронту понадобится словарь переопределения имён — вернуть параметр |
|
||||
| 79 | 2026-10-06 | Сквозной тест на фикстурах (Task 13): ГК «Русский бисквит» сидится в Ростовском филиале, а не в «СПб РФ», как в плане | dev-пользователь `ka_rf` (seed) — сотрудник Ростовского филиала; видимость ГК и документов у РФ ограничена своим филиалом (policy), у `ka_rf` нет СПб; смысл сценария (КА РФ ведёт свою ГК) не меняется | тест не проверяет конкретное название филиала; при появлении dev-пользователя СПб РФ — заменить константу `ROSTOV` |
|
||||
| 80 | 2026-10-06 | Сверка сопоставления на реальных данных: `unmapped_total` падает на число СКЛЕЕННЫХ строк свода, получивших контрагента, а `1 + propagated` равно числу записей `osv_entry` с этим именем в ОСВ РБ, привязанных к версии (включая пары-пробелы 66/76, строк в своде не дающие); равенство «`unmapped_total` падает на `1 + propagated`» из spec consolidated-osv «Сопоставление из плана среза» верно только для нарастающих документов (одна запись = одна строка свода) | на фикстурах квартальные записи одного контрагента склеиваются (D-2): в РБ «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» — 18 записей, 4 строки свода по счетам 60/62; поведение продукта (propagated считает записи хранилища, summary — строки свода) согласовано с D-2/D-3, неточна формулировка spec | формулировка сценария в spec вводит в заблуждение для квартальных документов; уточнить при архивации (не меняет код) |
|
||||
| 81 | 2026-10-06 | R-14: документ за `Q1` (`period_quarter = Q1`) равнозначен и нарастающему, и квартальному. Одна функция `consolidation_periods.effective_cumulative(is_cumulative, period_quarter)` (ложна для `Q1`, иначе `counts_as_cumulative`) используется в `fits` (документ за `Q1` подходит любой версии года при любом флаге, прочие — как раньше), в ключе замены `_key` (нарастающий и квартальный `Q1` одной пары — один слот, побеждает поздний `confirmed_at`, при равенстве — больший `id`), в `select_sources` (для версии `Q2+` документ за `Q1` — квартальный источник `Q1`; нарастающий за квартал версии по-прежнему побеждает; для версии `Q1` — прежнее поведение: нарастающий «как есть», квартальный — склейка, №50) и в адаптере чек-листа | парсер помечает ОСВ за первый квартал нарастающей (период с начала года = Q1); без этого сквозной тест на фикстурах РБ давал 17 привязок из 22, пары 60/62(/76) уходили в пробел `[Q1]`, свод пуст | нарастающий `Q1` и квартальный `Q1` одной пары вытесняют друг друга при привязке; правило распространяется и на ББ/ОФР за `Q1` (`is_cumulative` NULL): они тоже привязываются к версиям `Q2`–`Q4` и подтверждение такого документа двигает версию в `DATA_CORRECTION` (ячейки ББ/ОФР чек-листа не затронуты — они требуют квартала версии, №49); если владелец хочет иное — меняется только `effective_cumulative` |
|
||||
| 81 | 2026-10-06 | R-14 + R-14a: ОСВ за `Q1` (`period_quarter = Q1`) равнозначна и нарастающей, и квартальной. `consolidation_periods.effective_cumulative(is_cumulative, period_quarter)` (ложна для `Q1`, иначе `counts_as_cumulative`) используется в `select_sources` (для версии `Q2+` ОСВ за `Q1` — квартальный источник `Q1`; нарастающая за квартал версии по-прежнему побеждает; для версии `Q1` — прежнее поведение: нарастающая «как есть», квартальная — склейка, №50), в адаптере чек-листа и — только для `document_type = OSV` — в `consolidation_documents._cumulative`, то есть в `fits` (ОСВ за `Q1` подходит любой версии года при любом флаге) и в ключе замены `_key` (нарастающая и квартальная ОСВ за `Q1` одной пары — один слот, побеждает поздний `confirmed_at`, при равенстве — больший `id`). R-14a: ББ/ОФР за `Q1` — отчёт на конец квартала, не источник версий `Q2`–`Q4`: для них прежнее правило D-5 (NULL → нарастающий → только квартал версии), ключ замены не меняется | парсер помечает ОСВ за первый квартал нарастающей (период с начала года = Q1); без этого сквозной тест на фикстурах РБ давал 17 привязок из 22, пары 60/62(/76) уходили в пробел `[Q1]`, свод пуст | нарастающая и квартальная ОСВ за `Q1` одной пары вытесняют друг друга при привязке; если и ББ/ОФР за `Q1` должны подхватываться версиями `Q2`–`Q4` — снять условие `document_type = OSV` в `_cumulative` |
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
привязанных к версии (`consolidation_document`): документ с `is_cumulative = true` и `period_quarter` = квартал версии
|
||||
→ единственный источник; иначе квартальные документы (`is_cumulative = false`) за каждый квартал от `Q1` до квартала
|
||||
версии включительно; для версии за `Q1` квартальный документ `Q1` равнозначен нарастающему; для версии за `Q2`–`Q4`
|
||||
документ за `Q1` с любым `is_cumulative` служит квартальным `Q1` (нарастающий с начала года за первый квартал и есть
|
||||
ОСВ за `Q1` с любым `is_cumulative` служит квартальной `Q1` (нарастающий с начала года за первый квартал и есть
|
||||
квартал `Q1`, R-14); при наличии и
|
||||
нарастающего, и квартальных — нарастающий. Если какого-то квартала нет, пара SHALL попадать в `gaps` с
|
||||
`missing_quarters`, а строки по ней MUST NOT строиться (решение владельца 06.10.2026, OQ-03). Квартальные строки
|
||||
|
||||
@ -12,10 +12,11 @@
|
||||
В той же транзакции документ SHALL привязываться (`consolidation_document`) ко всем версиям «в работе» (`INITIATED`,
|
||||
`DOCUMENTS_PROCESSING`, `DATA_CORRECTION`), которым он подходит: ЮЛ документа ∈ `consolidation_member` версии,
|
||||
`period_year` = год версии, `period_quarter` не NULL и (`is_cumulative` true или NULL → `period_quarter` = квартал
|
||||
версии; false → `period_quarter` ≤ квартала версии). Документ за `Q1` равнозначен нарастающему и квартальному (R-14):
|
||||
он подходит любой версии года при любом `is_cumulative`, а нарастающий и квартальный `Q1` одной пары (ЮЛ, счёт) —
|
||||
одна привязка (подтверждённый позже вытесняет прежний). Привязанный ранее документ того же ЮЛ с той же тройкой
|
||||
(`document_type`, `osv_account_code`, `period_quarter`) в той же версии SHALL отвязываться. Каждая версия, получившая
|
||||
версии; false → `period_quarter` ≤ квартала версии). ОСВ за `Q1` равнозначна нарастающей и квартальной (R-14): она
|
||||
подходит любой версии года при любом `is_cumulative`; ББ/ОФР за `Q1` — отчёт на конец квартала и подходят только версии
|
||||
за `Q1` (R-14a). Привязанный ранее документ того же ЮЛ с тем же ключом (`document_type`, `osv_account_code`,
|
||||
`period_quarter`, признак нарастающего: `is_cumulative` true или NULL; для ОСВ за `Q1` признак не различается) в той же
|
||||
версии SHALL отвязываться (R-1). Каждая версия, получившая
|
||||
привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` (`changed_by` =
|
||||
подтвердивший). Блокировка версии (`consolidation_lock`) для подтверждения MUST NOT требоваться.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user