fix(consolidation): документ за Q1 равнозначен нарастающему и квартальному (R-14)
Сквозной тест на фикстурах РБ показал: парсер помечает ОСВ за Q1 нарастающей, а привязка (`fits`) брала нарастающий только за квартал версии, `select_sources` не считал его квартальным Q1 — для версии Q3 привязано 17 из 22, пары 60/62(/76) в пробеле [Q1], свод пуст. - consolidation_periods.effective_cumulative: для Q1 — квартальный при любом флаге; одна функция для `fits`, ключа замены `_key`, `select_sources` (версии Q2+) и адаптера чек-листа; для версии Q1 поведение прежнее (№50) - тесты: таблица effective_cumulative, select_sources/build/чек-лист с нарастающим Q1, fits любой версии года, подтверждение и подхват (нарастающий и квартальный Q1 — один слот, побеждает поздний) - test_consolidation_fixtures: xfail снят, сценарий зелёный (22 привязки, 1385 строк) - спеки documents-api и consolidated-osv: фраза о равнозначности Q1; design #81; план Task 13 Step 2–4 и tasks.md 5.1 отмечены Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
af91643811
commit
d3c4964e92
@ -23,7 +23,7 @@ from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
from src.domain.consolidation_periods import Sources, quarters_through, select_sources
|
||||
from src.domain.consolidation_periods import Sources, effective_cumulative, quarters_through, select_sources
|
||||
from src.domain.osv_mapping import account_sort_key
|
||||
from src.models.enums import DocumentStatus, DocumentType, FileStatus, PeriodQuarter
|
||||
|
||||
@ -130,14 +130,18 @@ def _osv_coverage(version_quarter: PeriodQuarter, documents: Sequence[DocumentLi
|
||||
|
||||
Полное покрытие — источники без пробела (нарастающий за квартал версии или квартальные `Q1..Qn` все). При пробеле
|
||||
`Gap` документов не несёт, поэтому адаптер сам берёт документы имеющихся кварталов (квартальные `Q1..Qn` в порядке
|
||||
кварталов) и перечисляет `missing_quarters` только при частичном покрытии: без единого документа — `()` (№46).
|
||||
кварталов; документ за `Q1` — квартальный при любом флаге, R-14) и перечисляет `missing_quarters` только при
|
||||
частичном покрытии: без единого документа — `()` (№46).
|
||||
"""
|
||||
selected = select_sources(version_quarter, documents)
|
||||
if isinstance(selected, Sources):
|
||||
return _Coverage(True, (), tuple(d.id for d in selected.documents))
|
||||
needed = quarters_through(version_quarter)
|
||||
present = tuple(
|
||||
d.id for quarter in needed for d in documents if d.is_cumulative is False and d.period_quarter == quarter
|
||||
d.id
|
||||
for quarter in needed
|
||||
for d in documents
|
||||
if not effective_cumulative(d.is_cumulative, d.period_quarter) and d.period_quarter == quarter
|
||||
)
|
||||
return _Coverage(False, selected.missing_quarters if present else (), present)
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ __all__ = [
|
||||
"Gap",
|
||||
"Sources",
|
||||
"counts_as_cumulative",
|
||||
"effective_cumulative",
|
||||
"quarters_through",
|
||||
"reporting_date",
|
||||
"select_sources",
|
||||
@ -56,6 +57,17 @@ def counts_as_cumulative(is_cumulative: bool | None) -> bool:
|
||||
return is_cumulative is not False
|
||||
|
||||
|
||||
def effective_cumulative(is_cumulative: bool | None, period_quarter: PeriodQuarter | None) -> bool:
|
||||
"""Нарастающий ли документ для привязки, ключа замены и выбора квартальных источников (R-14).
|
||||
|
||||
Документ за `Q1` равнозначен и нарастающему, и квартальному (нарастающий с начала года за первый квартал и есть
|
||||
квартал `Q1`; парсер помечает такие ОСВ нарастающими): здесь он считается квартальным при любом флаге — подходит
|
||||
любой версии года, а нарастающий и квартальный `Q1` одной пары — один слот привязки. Прочие —
|
||||
`counts_as_cumulative`.
|
||||
"""
|
||||
return period_quarter != PeriodQuarter.Q1 and counts_as_cumulative(is_cumulative)
|
||||
|
||||
|
||||
class DocLike(Protocol):
|
||||
"""Документ-кандидат в источники (структурный тип): поля — как в DBML `document`."""
|
||||
|
||||
@ -90,10 +102,11 @@ def select_sources(version_quarter: PeriodQuarter, documents: Sequence[D]) -> So
|
||||
"""Выбор источников одной пары (ЮЛ, счёт) по документам, привязанным к версии (D-2, §2, OQ-03).
|
||||
|
||||
1. Нарастающий (`is_cumulative` true/NULL) за квартал версии есть → он один (`cumulative = True`), квартальные
|
||||
рядом игнорируются (нарастающий полнее по определению).
|
||||
2. Иначе нужны квартальные (`is_cumulative = False`) за каждый квартал `Q1..Qn` версии; для версии `Q1` квартальный
|
||||
`Q1` равнозначен нарастающему. Все есть → `Sources(cumulative=False)` в порядке кварталов (внутри квартала —
|
||||
порядок входа).
|
||||
рядом игнорируются (нарастающий полнее по определению). Для версии `Q1` это нарастающий `Q1` «как есть».
|
||||
2. Иначе нужны квартальные за каждый квартал `Q1..Qn` версии (`effective_cumulative` ложен: `is_cumulative = False`
|
||||
или любой документ за `Q1` — R-14, нарастающий `Q1` для версий `Q2+` служит квартальным `Q1`); для версии `Q1`
|
||||
квартальный `Q1` равнозначен нарастающему. Все есть → `Sources(cumulative=False)` в порядке кварталов (внутри
|
||||
квартала — порядок входа).
|
||||
3. Какого-то квартала нет → `Gap(missing_quarters)`; при отсутствии подходящих документов — все кварталы `Q1..Qn`.
|
||||
|
||||
Документы без квартала, нарастающие за другой квартал и квартальные позже версии источниками не бывают (такие
|
||||
@ -106,7 +119,11 @@ def select_sources(version_quarter: PeriodQuarter, documents: Sequence[D]) -> So
|
||||
if cumulative:
|
||||
return Sources(documents=cumulative, cumulative=True)
|
||||
needed = quarters_through(version_quarter)
|
||||
quarterly = [d for d in documents if d.is_cumulative is False and d.period_quarter in needed]
|
||||
quarterly = [
|
||||
d
|
||||
for d in documents
|
||||
if not effective_cumulative(d.is_cumulative, d.period_quarter) and d.period_quarter in needed
|
||||
]
|
||||
missing = tuple(quarter for quarter in needed if all(d.period_quarter != quarter for d in quarterly))
|
||||
if missing:
|
||||
return Gap(missing_quarters=missing)
|
||||
|
||||
@ -8,8 +8,9 @@
|
||||
(`unbind_entity`). Версии вне «в работе» не трогаются.
|
||||
|
||||
Ключ замены (R-1): у одного ЮЛ в одной версии на ключ `(document_type, osv_account_code, period_quarter, cumulative)`
|
||||
привязан один документ — с самым поздним `confirmed_at`; `cumulative = is_cumulative is not False` (NULL — ББ/ОФР —
|
||||
нарастающий, решение №10). Квартальный и нарастающий документы одного квартала друг друга не вытесняют.
|
||||
привязан один документ — с самым поздним `confirmed_at`; `cumulative = effective_cumulative(...)` (NULL — ББ/ОФР —
|
||||
нарастающий, решение №10). Квартальный и нарастающий документы одного квартала друг друга не вытесняют — кроме `Q1`:
|
||||
документ за `Q1` равнозначен обоим видам (R-14), нарастающий и квартальный `Q1` — один слот, побеждает поздний.
|
||||
|
||||
Порядок блокировок (D-9, R-2, R-3): файл → документ (`FOR NO KEY UPDATE`) → advisory ГК → версии `FOR UPDATE` по `id`
|
||||
(`repo.lock_versions_for_entity`); автозамена — advisory ЮЛ → старые файлы → advisory ГК → версии; создание версии —
|
||||
@ -27,7 +28,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, counts_as_cumulative
|
||||
from src.domain.consolidation_periods import QUARTER_ORDER, 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
|
||||
@ -56,8 +57,9 @@ def _rowcount(result: Any) -> int:
|
||||
|
||||
|
||||
def _cumulative(document: Document) -> bool:
|
||||
"""Нарастающий ли документ (R-1, решение №10): `consolidation_periods.counts_as_cumulative` — NULL нарастающий."""
|
||||
return counts_as_cumulative(document.is_cumulative)
|
||||
"""Нарастающий ли документ для привязки (R-1, решение №10, R-14): `consolidation_periods.effective_cumulative` —
|
||||
NULL нарастающий; документ за `Q1` — квартальный при любом флаге."""
|
||||
return effective_cumulative(document.is_cumulative, document.period_quarter)
|
||||
|
||||
|
||||
def _key(document: Document) -> BindingKey:
|
||||
@ -84,7 +86,8 @@ def fits(
|
||||
member_ids: Collection[uuid.UUID],
|
||||
) -> bool:
|
||||
"""Подходит ли документ версии (D-5): ЮЛ — член периметра, год версии, квартал определён и: нарастающий (или
|
||||
NULL) — квартал версии; квартальный — не позже квартала версии. Статус документа и файла не проверяется — это
|
||||
NULL) — квартал версии; квартальный — не позже квартала версии; документ за `Q1` — любой версии года при любом
|
||||
флаге (R-14). Статус документа и файла не проверяется — это
|
||||
дело вызывающего (привязываются только `CONFIRMED` незаменённых файлов). Чистая функция, без БД."""
|
||||
if document.legal_entity_id not in member_ids or document.period_year != consolidation.year:
|
||||
return False
|
||||
|
||||
@ -101,6 +101,10 @@ def _pure_document(
|
||||
(_pure_document(cumulative=False, quarter=None), False), # квартал не определён
|
||||
(_pure_document(cumulative=None, quarter=Q3), True), # ББ/ОФР: NULL — нарастающий (решение №10)
|
||||
(_pure_document(cumulative=None, quarter=Q2), False), # NULL за Q2 при версии Q3 — как нарастающий
|
||||
(_pure_document(cumulative=True, quarter=Q1), True), # R-14: нарастающий Q1 = квартальный Q1, подходит Q3
|
||||
(_pure_document(cumulative=None, quarter=Q1), True), # R-14: флаг за Q1 не различает
|
||||
(_pure_document(year=YEAR - 1, cumulative=True, quarter=Q1), False), # Q1 другого года
|
||||
(_pure_document(entity=_STRANGER, cumulative=True, quarter=Q1), False), # Q1 не члена периметра
|
||||
],
|
||||
)
|
||||
def test_fits(document: Document, expected: bool) -> None:
|
||||
@ -200,6 +204,16 @@ async def _bind(session: AsyncSession, version: ConsolidationVersion, document:
|
||||
await session.flush()
|
||||
|
||||
|
||||
@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 равнозначен нарастающему и квартальному — подходит любой версии года."""
|
||||
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
|
||||
|
||||
|
||||
# --- подтверждение ----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@ -255,6 +269,41 @@ async def test_confirm_binds_in_other_in_work_statuses(
|
||||
assert len(await _history(seeded, version.id)) == before + (1 if moved else 0)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_confirm_binds_cumulative_q1_to_version_q3(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||||
"""R-14 (фикстуры РБ): нарастающая ОСВ за Q1 привязывается к версии Q3 как квартальный Q1 и двигает версию."""
|
||||
group, entities = await _group(seeded)
|
||||
version = await _version(seeded, group, entities)
|
||||
document = await _document(seeded, entities[0], quarter=Q1, cumulative=True)
|
||||
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) == {document.id}
|
||||
assert await _status(seeded, version.id) is ConsolidationStatus.DATA_CORRECTION
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("bound_cumulative", [False, True])
|
||||
async def test_confirm_q1_replaces_bound_q1_of_other_flag(
|
||||
seeded: AsyncSession, auth_client: AuthClientFactory, bound_cumulative: bool
|
||||
) -> None:
|
||||
"""R-14: нарастающий и квартальный Q1 одной пары (ЮЛ, счёт) — один слот привязки: подтверждённый позже вытесняет
|
||||
привязанный."""
|
||||
group, entities = await _group(seeded)
|
||||
version = await _version(seeded, group, entities, status=ConsolidationStatus.DATA_CORRECTION)
|
||||
old = await _confirmed(seeded, entities[0], quarter=Q1, cumulative=bound_cumulative)
|
||||
await _bind(seeded, version, old)
|
||||
new = await _document(seeded, entities[0], quarter=Q1, cumulative=not bound_cumulative)
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
response = await client.post(f"/documents/{new.id}/confirm")
|
||||
|
||||
assert response.status_code == HTTP_OK, response.text
|
||||
assert await _bound(seeded, version.id) == {new.id}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_confirm_does_not_bind_cumulative_of_earlier_quarter(
|
||||
seeded: AsyncSession, auth_client: AuthClientFactory
|
||||
@ -487,6 +536,32 @@ async def test_bind_confirmed_replaces_older_binding_of_same_key(seeded: AsyncSe
|
||||
assert await _bound(seeded, version.id) == {newer.id}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("later_cumulative", [True, False])
|
||||
async def test_bind_confirmed_q1_of_both_flags_is_one_slot(seeded: AsyncSession, later_cumulative: bool) -> None:
|
||||
"""R-14: подхват из нарастающего и квартального Q1 одной пары берёт один — с более поздним `confirmed_at`."""
|
||||
group, entities = await _group(seeded)
|
||||
version = await _version(seeded, group, entities)
|
||||
consolidation = await seeded.get(Consolidation, version.consolidation_id)
|
||||
assert consolidation is not None
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
later = await _confirmed(seeded, entities[0], quarter=Q1, cumulative=later_cumulative, confirmed_at=now)
|
||||
await _confirmed(
|
||||
seeded,
|
||||
entities[0],
|
||||
quarter=Q1,
|
||||
cumulative=not later_cumulative,
|
||||
confirmed_at=now - datetime.timedelta(hours=1),
|
||||
)
|
||||
|
||||
count = await consolidation_documents.bind_confirmed(
|
||||
seeded, version=version, consolidation=consolidation, entity_ids=[entities[0].id]
|
||||
)
|
||||
|
||||
assert count == 1
|
||||
assert await _bound(seeded, version.id) == {later.id}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_bind_confirmed_tie_on_confirmed_at_takes_larger_id(seeded: AsyncSession) -> None:
|
||||
"""Решение №35: при равном `confirmed_at` привязывается документ с большим `id`."""
|
||||
|
||||
@ -101,16 +101,6 @@ async def _all_rows(client: AsyncClient, version_url: str, **params: Any) -> lis
|
||||
return items
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
strict=True,
|
||||
reason=(
|
||||
"ДЕФЕКТ Срезов 4 (D-5/D-2): golden ОСВ Q1 помечены is_cumulative=true (парсер: первый квартал нарастающий), а "
|
||||
"`consolidation_documents.fits` привязывает нарастающий документ только за квартал версии, и "
|
||||
"`consolidation_periods.select_sources` не считает нарастающий Q1 квартальным — для версии Q3 документы Q1 не "
|
||||
"привязаны (17 из 22), пары 60/62/76 уходят в gaps [Q1], rows_total = 0. Снять xfail после исправления: "
|
||||
"нарастающий Q1 равнозначен квартальному Q1 для любой версии года (fits + select_sources)"
|
||||
),
|
||||
)
|
||||
async def test_consolidation_on_russian_biscuit_fixtures( # noqa: PLR0912, PLR0914, PLR0915 - сквозной сценарий в одном месте
|
||||
auth_client: AuthClientFactory, seeded: AsyncSession, fixtures_dir: Path
|
||||
) -> None:
|
||||
|
||||
@ -99,6 +99,25 @@ def test_cumulative_of_other_quarter_does_not_cover() -> None:
|
||||
assert cell == Cell(CellStatus.MISSING)
|
||||
|
||||
|
||||
def test_cumulative_q1_with_quarterly_q2_q3_confirms() -> None:
|
||||
"""R-14: нарастающий Q1 (так парсер помечает первый квартал) равнозначен квартальному Q1 для версии Q3."""
|
||||
entity = _entity()
|
||||
documents = [_doc(entity, quarter=Q1, cumulative=True), _doc(entity, quarter=Q2), _doc(entity, quarter=Q3)]
|
||||
|
||||
cell = _cell(OSV_60, entity, bound=documents)
|
||||
|
||||
assert cell == Cell(CellStatus.CONFIRMED, (), tuple(d.id for d in documents))
|
||||
|
||||
|
||||
def test_cumulative_q1_counts_as_present_quarter_of_gap() -> None:
|
||||
entity = _entity()
|
||||
first, third = _doc(entity, quarter=Q1, cumulative=True), _doc(entity, quarter=Q3)
|
||||
|
||||
cell = _cell(OSV_60, entity, bound=[third, first])
|
||||
|
||||
assert cell == Cell(CellStatus.MISSING, (Q2,), (first.id, third.id))
|
||||
|
||||
|
||||
def test_q1_version_quarterly_q1_equals_cumulative() -> None:
|
||||
entity = _entity()
|
||||
document = _doc(entity, quarter=Q1, cumulative=False)
|
||||
|
||||
@ -206,6 +206,30 @@ def test_cumulative_next_to_quarterly_rows_as_is() -> None:
|
||||
assert result.gaps == ()
|
||||
|
||||
|
||||
def test_cumulative_q1_with_quarterly_q2_q3_is_glued() -> None:
|
||||
"""R-14, форма фикстур РБ: ОСВ Q1 помечена нарастающей, Q2/Q3 — квартальные; версия Q3 — склейка: opening из Q1,
|
||||
turnover = Σ, closing из Q3, строка квартальная; пробела нет."""
|
||||
q1, q2, q3 = _doc(quarter=Q1, cumulative=True), _doc(quarter=Q2), _doc(quarter=Q3)
|
||||
r1 = _row(q1, od="100", oc="5", td="200", tc="7", cd="300", cc="12")
|
||||
r2 = _row(q2, od="300", oc="12", td="150", tc=None, cd="450", cc="12")
|
||||
r3 = _row(q3, od="450", oc="12", td="50", tc="3", cd="500", cc="15")
|
||||
result = _build([q1, q2, q3], [r3, r1, r2])
|
||||
assert result.gaps == ()
|
||||
assert len(result.rows) == 1
|
||||
row = result.rows[0]
|
||||
assert _sums(row) == (Decimal(100), Decimal(5), Decimal(400), Decimal(10), Decimal(500), Decimal(15))
|
||||
assert row.is_cumulative is False
|
||||
assert row.entry_ids == (r1.id, r2.id, r3.id)
|
||||
|
||||
|
||||
def test_cumulative_q1_at_version_q1_is_taken_as_is() -> None:
|
||||
q1 = _doc(quarter=Q1, cumulative=True)
|
||||
source = _row(q1, od="1", oc=None, td="2", tc=None, cd="3", cc=None)
|
||||
row = _build([q1], [source], quarter=Q1).rows[0]
|
||||
assert row.is_cumulative is True
|
||||
assert _sums(row) == (Decimal(1), None, Decimal(2), None, Decimal(3), None)
|
||||
|
||||
|
||||
def test_cumulative_document_keeps_duplicate_keys_separate() -> None:
|
||||
"""Решение №1: дубли ключа в нарастающем документе не суммируются — строки свода = строки файла (§3 п. 6)."""
|
||||
cumulative = _doc(cumulative=True)
|
||||
|
||||
@ -11,6 +11,7 @@ from src.domain.consolidation_periods import (
|
||||
Gap,
|
||||
Sources,
|
||||
counts_as_cumulative,
|
||||
effective_cumulative,
|
||||
quarters_through,
|
||||
reporting_date,
|
||||
select_sources,
|
||||
@ -115,3 +116,53 @@ def test_quarterly_after_version_and_without_quarter_are_ignored() -> None:
|
||||
def test_no_documents_is_gap_of_all_quarters() -> None:
|
||||
assert select_sources(Q3, []) == Gap(missing_quarters=(Q1, Q2, Q3))
|
||||
assert select_sources(Q1, []) == Gap(missing_quarters=(Q1,))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------- R-14: документ за Q1 равнозначен обоим видам
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("flag", "quarter", "expected"),
|
||||
[
|
||||
(True, Q1, False), # нарастающий с начала года за Q1 — это и есть квартал Q1
|
||||
(None, Q1, False),
|
||||
(False, Q1, False),
|
||||
(True, Q2, True),
|
||||
(None, Q3, True), # ББ/ОФР вне Q1 — нарастающие (решение №10)
|
||||
(False, Q3, False),
|
||||
(True, None, True), # квартал не определён — по флагу (такой документ не привязывается)
|
||||
],
|
||||
)
|
||||
def test_effective_cumulative(flag: bool | None, quarter: PeriodQuarter | None, expected: bool) -> None:
|
||||
assert effective_cumulative(flag, quarter) is expected
|
||||
|
||||
|
||||
def test_cumulative_q1_is_quarterly_q1_source_for_later_version() -> None:
|
||||
"""Фикстуры РБ: парсер помечает ОСВ за Q1 нарастающей, Q2/Q3 — квартальными; для версии Q3 это полный комплект."""
|
||||
q1, q2, q3 = _doc(Q1, True), _doc(Q2), _doc(Q3)
|
||||
assert select_sources(Q3, [q3, q2, q1]) == Sources(documents=(q1, q2, q3), cumulative=False)
|
||||
assert select_sources(Q2, [q2, q1]) == Sources(documents=(q1, q2), cumulative=False)
|
||||
|
||||
|
||||
def test_null_flag_q1_is_quarterly_q1_source_for_later_version() -> None:
|
||||
q1, q2 = _doc(Q1, None), _doc(Q2)
|
||||
assert select_sources(Q2, [q1, q2]) == Sources(documents=(q1, q2), cumulative=False)
|
||||
|
||||
|
||||
def test_cumulative_q1_does_not_fill_other_quarters() -> None:
|
||||
assert select_sources(Q2, [_doc(Q1, True)]) == Gap(missing_quarters=(Q2,))
|
||||
# нарастающий Q2 при версии Q3 — не квартальный Q2: не хватает квартальных Q2 и Q3
|
||||
assert select_sources(Q3, [_doc(Q1, True), _doc(Q2, True)]) == Gap(missing_quarters=(Q2, Q3))
|
||||
|
||||
|
||||
def test_cumulative_of_version_quarter_still_beats_cumulative_q1() -> None:
|
||||
cumulative = _doc(Q3, True)
|
||||
assert select_sources(Q3, [_doc(Q1, True), _doc(Q2), _doc(Q3), cumulative]) == Sources((cumulative,), True)
|
||||
|
||||
|
||||
def test_q1_version_cumulative_q1_is_taken_as_is() -> None:
|
||||
"""Версия Q1: нарастающий Q1 — источник «как есть» (решение №50), квартальный — склейка из одного квартала."""
|
||||
cumulative, quarterly = _doc(Q1, True), _doc(Q1, False)
|
||||
assert select_sources(Q1, [cumulative]) == Sources((cumulative,), cumulative=True)
|
||||
assert select_sources(Q1, [quarterly, cumulative]) == Sources((cumulative,), cumulative=True)
|
||||
assert select_sources(Q1, [quarterly]) == Sources((quarterly,), cumulative=False)
|
||||
|
||||
@ -253,8 +253,8 @@
|
||||
- Время теста ≤ 60 с; выводить в лог число строк свода.
|
||||
|
||||
- [x] **Step 1: Тест** по сценарию (сначала падает на отсутствии хелпера или данных).
|
||||
- [ ] **Step 2–4:** хелпер → `pytest -m "integration and fixtures" tests/test_consolidation_fixtures.py -q` → PASS; `make check-full` (без `e2e`, если compose с парсером не поднят — отметить в ledger).
|
||||
- [x] **Step 5: Commit** `test(consolidation): сквозной сценарий Среза 4 на фикстурах группы «Русский бисквит»` (тест под `xfail(strict)`: найден дефект привязки Q1, см. `task-13-report.md`; Step 2–4 закрыть после исправления).
|
||||
- [x] **Step 2–4:** хелпер → `pytest -m "integration and fixtures" tests/test_consolidation_fixtures.py -q` → PASS; `make check-full` (без `e2e`, если compose с парсером не поднят — отметить в ledger).
|
||||
- [x] **Step 5: Commit** `test(consolidation): сквозной сценарий Среза 4 на фикстурах группы «Русский бисквит»` (тест под `xfail(strict)`: найден дефект привязки Q1, см. `task-13-report.md`; Step 2–4 закрыть после исправления; закрыты исправлением R-14 — документ за Q1 равнозначен нарастающему и квартальному, xfail снят).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -265,3 +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` |
|
||||
|
||||
@ -12,7 +12,9 @@
|
||||
Для каждой пары (ЮЛ периметра, `osv_account_code` документа) ядро SHALL выбирать источники среди ОСВ-документов,
|
||||
привязанных к версии (`consolidation_document`): документ с `is_cumulative = true` и `period_quarter` = квартал версии
|
||||
→ единственный источник; иначе квартальные документы (`is_cumulative = false`) за каждый квартал от `Q1` до квартала
|
||||
версии включительно; для версии за `Q1` квартальный документ `Q1` равнозначен нарастающему; при наличии и
|
||||
версии включительно; для версии за `Q1` квартальный документ `Q1` равнозначен нарастающему; для версии за `Q2`–`Q4`
|
||||
документ за `Q1` с любым `is_cumulative` служит квартальным `Q1` (нарастающий с начала года за первый квартал и есть
|
||||
квартал `Q1`, R-14); при наличии и
|
||||
нарастающего, и квартальных — нарастающий. Если какого-то квартала нет, пара SHALL попадать в `gaps` с
|
||||
`missing_quarters`, а строки по ней MUST NOT строиться (решение владельца 06.10.2026, OQ-03). Квартальные строки
|
||||
SHALL склеиваться по ключу (`account_code` строки, `counterparty_name.strip()`, ИНН без пробелов): `opening_*` из
|
||||
|
||||
@ -12,7 +12,9 @@
|
||||
В той же транзакции документ SHALL привязываться (`consolidation_document`) ко всем версиям «в работе» (`INITIATED`,
|
||||
`DOCUMENTS_PROCESSING`, `DATA_CORRECTION`), которым он подходит: ЮЛ документа ∈ `consolidation_member` версии,
|
||||
`period_year` = год версии, `period_quarter` не NULL и (`is_cumulative` true или NULL → `period_quarter` = квартал
|
||||
версии; false → `period_quarter` ≤ квартала версии). Привязанный ранее документ того же ЮЛ с той же тройкой
|
||||
версии; false → `period_quarter` ≤ квартала версии). Документ за `Q1` равнозначен нарастающему и квартальному (R-14):
|
||||
он подходит любой версии года при любом `is_cumulative`, а нарастающий и квартальный `Q1` одной пары (ЮЛ, счёт) —
|
||||
одна привязка (подтверждённый позже вытесняет прежний). Привязанный ранее документ того же ЮЛ с той же тройкой
|
||||
(`document_type`, `osv_account_code`, `period_quarter`) в той же версии SHALL отвязываться. Каждая версия, получившая
|
||||
привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` (`changed_by` =
|
||||
подтвердивший). Блокировка версии (`consolidation_lock`) для подтверждения MUST NOT требоваться.
|
||||
|
||||
@ -56,11 +56,11 @@
|
||||
|
||||
## 5. Сквозной тест и документы
|
||||
|
||||
- [ ] 5.1 **Фикстуры РБ.** `tests/test_consolidation_fixtures.py` (маркер `fixtures`): маппинг golden-снимков ОСВ РБ и
|
||||
- [x] 5.1 **Фикстуры РБ.** `tests/test_consolidation_fixtures.py` (маркер `fixtures`): маппинг golden-снимков ОСВ РБ и
|
||||
Счастливочной за Q1–Q3 2024 → подтверждение → консолидация 2024 Q3 → свод: счета 60/62 у обеих и 76 у Счастливочной,
|
||||
остальные в `gaps`; одно сопоставление «Пекарня Счастливочная ООО» → `GSK` у всех строк РБ с этим именем;
|
||||
экспорт открывается. НЕ закрыт: тест написан и помечен `xfail(strict)` — на реальных данных нарастающий Q1
|
||||
(`is_cumulative = true`) не привязывается к версии Q3 (`fits`) и не считается квартальным Q1 (`select_sources`);
|
||||
снять xfail после исправления.
|
||||
экспорт открывается. Найденный тестом дефект (нарастающий Q1 не привязывался к версии Q3 и не считался
|
||||
квартальным Q1) исправлен рулингом R-14 (design #81): документ за Q1 равнозначен нарастающему и квартальному;
|
||||
xfail снят.
|
||||
- [ ] 5.2 **Документы.** README (консолидации, статусы Среза 4, свод, сопоставление, экспорт, коды ошибок), CLAUDE.md
|
||||
(новые модули, порядок блокировок), `openspec` спеки; синхронизация с docs-change (OQ, §2/§3/§9).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user