test(consolidation): сквозной сценарий Среза 4 на фикстурах группы «Русский бисквит»
Хелпер map_golden_snapshot, сценарий маппинг golden -> подтверждение -> консолидация 2024 Q3 -> свод, сопоставление Счастливочной, экспорт, независимая сверка склейки периодов. Тест помечен xfail(strict): на реальных данных нарастающий Q1 (is_cumulative=true) не привязывается к версии Q3 и не считается квартальным (fits / select_sources) — свод пуст; подробности в отчёте задачи. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
5b3b1011cb
commit
af91643811
@ -1,7 +1,19 @@
|
||||
"""Поиск каталога реальных данных `fixtures` (соседний репозиторий workspace) для тестов с маркером `fixtures`."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.contract.result import RecognitionResult
|
||||
from src.models.directories import LegalEntity
|
||||
from src.models.files import Document
|
||||
|
||||
FIXTURES_ENV = "ARM_KA_FIXTURES"
|
||||
|
||||
@ -26,3 +38,49 @@ def fixtures_skip_reason() -> str | None:
|
||||
f"ARM_KA_FIXTURES недоступен: нет каталога {path} "
|
||||
f"(задайте {FIXTURES_ENV} или положите fixtures рядом с arm-core)"
|
||||
)
|
||||
|
||||
|
||||
def golden_result(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> "RecognitionResult":
|
||||
"""Golden-снимок парсера как `RecognitionResult`: подставляет `job_id`/`file_id` и версию сервиса."""
|
||||
from src.contract.result import RecognitionResult # noqa: PLC0415 - тяжёлые импорты лениво: цикл с conftest
|
||||
|
||||
data = json.loads(path.read_text("utf-8"))
|
||||
data["service"] = {"version": "golden", **data["service"]}
|
||||
return RecognitionResult.model_validate(data | {"job_id": str(job_id), "file_id": str(file_id)})
|
||||
|
||||
|
||||
async def map_golden_snapshot(
|
||||
session: AsyncSession, *, entity: "LegalEntity", path: Path, uploaded_by: uuid.UUID | None
|
||||
) -> "list[Document]":
|
||||
"""Golden-снимок `path` → файл ЮЛ `entity` и его документы ОСВ в БД теста (`flush`, без commit).
|
||||
|
||||
Как маппер: файл `PROCESSING` с текущим заданием `done`, затем боевой `map_file` с планом счетов seed и настоящим
|
||||
`CounterpartyIndex` (справочник читается на момент вызова — ЮЛ и алиасы должны быть созданы до маппинга).
|
||||
Возвращает документы файла (после автозамены прежних версий — только документы этого файла).
|
||||
"""
|
||||
# ленивые импорты: `conftest` импортирует этот модуль раньше `tests.factories` (цикл)
|
||||
from src.models.accounting import ChartOfAccount # noqa: PLC0415
|
||||
from src.models.enums import FileStatus, RecognitionStatus # noqa: PLC0415
|
||||
from src.models.files import Document # noqa: PLC0415
|
||||
from src.repositories.counterparties import CounterpartyIndex # noqa: PLC0415
|
||||
from src.services.result_mapping import map_file # noqa: PLC0415
|
||||
from tests.factories import make_stored_file # noqa: PLC0415
|
||||
|
||||
file, job = await make_stored_file(
|
||||
session,
|
||||
entity=entity,
|
||||
status=FileStatus.PROCESSING,
|
||||
job_status=RecognitionStatus.done,
|
||||
original_name=path.name,
|
||||
uploaded_by=uploaded_by,
|
||||
)
|
||||
chart = {
|
||||
code: account_id for code, account_id in await session.execute(select(ChartOfAccount.code, ChartOfAccount.id))
|
||||
}
|
||||
index = await CounterpartyIndex.load(session)
|
||||
result = golden_result(path, job_id=job.id, file_id=file.id)
|
||||
await map_file(session, job=job, file=file, result=result, chart=chart, index=index, now=datetime.now(UTC))
|
||||
documents = await session.execute(
|
||||
select(Document).where(Document.stored_file_id == file.id).order_by(Document.osv_account_code)
|
||||
)
|
||||
return list(documents.scalars())
|
||||
|
||||
297
api/tests/test_consolidation_fixtures.py
Normal file
297
api/tests/test_consolidation_fixtures.py
Normal file
@ -0,0 +1,297 @@
|
||||
"""Сквозной сценарий Среза 4 на реальных данных группы «Русский бисквит» (tasks 5.1; spec consolidated-osv «Признаки на
|
||||
фикстурах РБ», «Сопоставление из плана среза», «Рекомендуемая Счастливочная»; documents-api «Фикстуры, подтверждённые
|
||||
до консолидации»).
|
||||
|
||||
Golden-снимки парсера `rb/2024-Q1..Q3` и `schastl/2024-Q1..Q3` → боевой `map_file` → подтверждение через
|
||||
`POST /documents/{id}/confirm` → создание консолидации 2024 Q3 (подхват подтверждённых) → блокировка → сводная ОСВ,
|
||||
сводка и пробелы, ручное сопоставление «Пекарни Счастливочной» с автоприменением → xlsx-экспорт. Суммы свода сверяются с
|
||||
суммой квартальных значений, прочитанных из golden-JSON независимо от доменного кода.
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from loguru import logger
|
||||
from openpyxl import load_workbook
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.aliases import normalize_alias
|
||||
from src.domain.osv_mapping import normalize_account_code
|
||||
from src.models.access import AppUser
|
||||
from src.models.consolidation import ConsolidationDocument
|
||||
from src.models.directories import RegionalOffice
|
||||
from src.models.enums import CounterpartyMatchKind, DocumentStatus
|
||||
from src.models.files import Document
|
||||
from src.models.osv import OsvEntry
|
||||
from tests.conftest import AuthClientFactory
|
||||
from tests.factories import make_company_group, make_legal_entity, make_membership, make_related_group
|
||||
from tests.fixtures_helpers import map_golden_snapshot
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.fixtures]
|
||||
|
||||
HTTP_OK = 200
|
||||
HTTP_CREATED = 201
|
||||
GOLDEN = Path("golden") / "expected_osv_parse"
|
||||
KA_RF = "ka_rf@armka.local"
|
||||
ROSTOV = "Ростовский региональный филиал" # филиал dev-пользователя ka_rf (в плане — СПб РФ: у ka_rf его нет)
|
||||
RB_INN = "3528052718" # АО «Русский бисквит» (ЕЦП-выписка 01.04.26)
|
||||
SCHASTL_INN = "3528104476" # ООО «Пекарня Счастливочная» (там же)
|
||||
SCHASTL_FILE_NAME = "ПЕКАРНЯ СЧАСТЛИВОЧНАЯ" # в файлах РБ контрагент записан «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ»
|
||||
RB_NAME_IN_SCHASTL = "Русский бисквит"
|
||||
TIME_BUDGET_SECONDS = 60.0
|
||||
Q1, Q2, Q3 = "Q1", "Q2", "Q3"
|
||||
SUMS = ("opening_debit", "opening_credit", "turnover_debit", "turnover_credit", "closing_debit", "closing_credit")
|
||||
|
||||
|
||||
def _is_schastl(name: str | None) -> bool:
|
||||
return SCHASTL_FILE_NAME.casefold() in normalize_alias(name or "")
|
||||
|
||||
|
||||
def _golden_files(fixtures_dir: Path, folder: str) -> list[Path]:
|
||||
paths = [
|
||||
p for quarter in (1, 2, 3) for p in sorted((fixtures_dir / GOLDEN / folder / f"2024-Q{quarter}").glob("*.json"))
|
||||
]
|
||||
assert paths, folder
|
||||
return paths
|
||||
|
||||
|
||||
def _golden_rows(fixtures_dir: Path, folder: str, account_root: str) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Строки-контрагенты golden-снимков ОСВ счёта `account_root` по кварталам — для независимого расчёта."""
|
||||
result: dict[str, list[dict[str, Any]]] = {Q1: [], Q2: [], Q3: []}
|
||||
for path in _golden_files(fixtures_dir, folder):
|
||||
for sheet in json.loads(path.read_text("utf-8"))["documents"]:
|
||||
header = sheet["header"]
|
||||
if sheet["document_type"] != "OSV" or header["account_code_raw"].split(".")[0] != account_root:
|
||||
continue
|
||||
result[header["period_quarter"]].extend(r for r in sheet["rows"] if r["group_type"] == "COUNTERPARTY")
|
||||
return result
|
||||
|
||||
|
||||
def _key(row: dict[str, Any]) -> tuple[str, str, str]:
|
||||
return (
|
||||
normalize_account_code(row["account_code_raw"]),
|
||||
(row["counterparty_name"] or "").strip(),
|
||||
(row["counterparty_inn"] or "").replace(" ", ""),
|
||||
)
|
||||
|
||||
|
||||
def _dec(value: str | None) -> Decimal:
|
||||
return Decimal(value) if value is not None else Decimal(0)
|
||||
|
||||
|
||||
async def _get(client: AsyncClient, url: str, **params: Any) -> Any:
|
||||
response = await client.get(url, params=params)
|
||||
assert response.status_code == HTTP_OK, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
async def _all_rows(client: AsyncClient, version_url: str, **params: Any) -> list[dict[str, Any]]:
|
||||
page = await _get(client, f"{version_url}/osv", limit=2000, **params)
|
||||
assert page["total"] == len(page["items"]), "свод не помещается в одну страницу"
|
||||
items: list[dict[str, Any]] = page["items"]
|
||||
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:
|
||||
started = time.perf_counter()
|
||||
office = (await seeded.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
|
||||
user = (await seeded.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
|
||||
group = await make_company_group(seeded, name="ГК Русский бисквит", office=office)
|
||||
gsk = await make_related_group(seeded, group=group, name="ГСК Русский бисквит")
|
||||
rb = await make_legal_entity(seeded, name="АО «Русский бисквит»", inn=RB_INN, group=group)
|
||||
schastl = await make_legal_entity(seeded, name="ООО «Пекарня Счастливочная»", inn=SCHASTL_INN, group=group)
|
||||
for entity in (rb, schastl):
|
||||
await make_membership(seeded, related_group=gsk, entity=entity)
|
||||
|
||||
documents: list[Document] = []
|
||||
for folder, entity in (("rb", rb), ("schastl", schastl)):
|
||||
for path in _golden_files(fixtures_dir, folder):
|
||||
documents.extend(await map_golden_snapshot(seeded, entity=entity, path=path, uploaded_by=user.id))
|
||||
assert len(documents) == 11 + 11 # РБ: Q1 2, Q2 2, Q3 7; Счастливочная: Q1 3, Q2 3, Q3 5
|
||||
|
||||
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, (document.osv_account_code, document.period_quarter, response.text)
|
||||
|
||||
created = await client.post(
|
||||
"/consolidations",
|
||||
json={"company_group_id": str(group.id), "related_group_id": str(gsk.id), "year": 2024, "quarter": "Q3"},
|
||||
)
|
||||
assert created.status_code == HTTP_CREATED, created.text
|
||||
card = created.json()
|
||||
assert card["status"] == "INITIATED"
|
||||
version_id = card["id"]
|
||||
version_url = f"/consolidations/versions/{version_id}"
|
||||
bound = (
|
||||
await seeded.execute(
|
||||
select(func.count())
|
||||
.select_from(ConsolidationDocument)
|
||||
.where(ConsolidationDocument.consolidation_version_id == uuid.UUID(version_id))
|
||||
)
|
||||
).scalar_one()
|
||||
logger.info("привязано документов к версии: {}", bound)
|
||||
assert bound == len(documents) # «Фикстуры, подтверждённые до консолидации»: подхвачены все подтверждённые ОСВ
|
||||
|
||||
locked = await client.post(f"{version_url}/lock")
|
||||
assert locked.status_code == HTTP_CREATED, locked.text
|
||||
|
||||
summary = await _get(client, f"{version_url}/osv/summary")
|
||||
rows = await _all_rows(client, version_url)
|
||||
logger.info(
|
||||
"сводная ОСВ РБ 2024 Q3: строк {}, сводка {}", len(rows), {k: v for k, v in summary.items() if k != "gaps"}
|
||||
)
|
||||
print(
|
||||
f"\nSVOD: rows={len(rows)} summary={ {k: v for k, v in summary.items() if k != 'gaps'} } gaps={summary['gaps']}"
|
||||
) # noqa: T201
|
||||
assert summary["rows_total"] == len(rows)
|
||||
|
||||
# строки свода: 60 и 62 у обеих, 76 у Счастливочной
|
||||
present = {(row["legal_entity_id"], row["account_root"]) for row in rows}
|
||||
for account in ("60", "62"):
|
||||
assert (str(rb.id), account) in present
|
||||
assert (str(schastl.id), account) in present
|
||||
assert (str(schastl.id), "76") in present
|
||||
|
||||
# пробелы: у РБ нет квартальных ОСВ 66/67/76 за Q1–Q2, у Счастливочной — 01/02
|
||||
gaps = {(g["legal_entity_id"], g["account_code"]): g["missing_quarters"] for g in summary["gaps"]}
|
||||
for account in ("66", "67", "76"):
|
||||
assert set(gaps[(str(rb.id), account)]) >= {Q1, Q2}
|
||||
for account in ("01", "02"):
|
||||
assert set(gaps[(str(schastl.id), account)]) >= {Q1, Q2}
|
||||
|
||||
# чек-лист: ОСВ 58 РБ не подтверждена (есть только 58.03)
|
||||
checklist = await _get(client, f"{version_url}/checklist")
|
||||
column = next(
|
||||
i for i, rule in enumerate(checklist["rules"]) if (rule["document_type"], rule["account"]) == ("OSV", "58")
|
||||
)
|
||||
rb_row = next(r for r in checklist["rows"] if r["legal_entity_id"] == str(rb.id))
|
||||
assert rb_row["cells"][column]["status"] != "CONFIRMED"
|
||||
for account in ("60", "62"): # ОСВ 60/62 РБ за Q1–Q3 подтверждены до консолидации — ячейка CONFIRMED, три документа
|
||||
cell = rb_row["cells"][
|
||||
next(i for i, r in enumerate(checklist["rules"]) if (r["document_type"], r["account"]) == ("OSV", account))
|
||||
]
|
||||
assert (cell["status"], len(cell["document_ids"])) == ("CONFIRMED", 3), account # noqa: PLR2004
|
||||
|
||||
# несопоставленные строки РБ «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ»
|
||||
unmapped = await _all_rows(client, version_url, unmapped="true", legal_entity_id=str(rb.id))
|
||||
target = [row for row in unmapped if _is_schastl(row["counterparty_name"])]
|
||||
assert target
|
||||
first = target[0]
|
||||
unmapped_before = summary["unmapped_total"]
|
||||
|
||||
mapped = await client.post(
|
||||
f"{version_url}/osv/entries/{first['entry_ids'][0]}/counterparty", json={"legal_entity_id": str(schastl.id)}
|
||||
)
|
||||
assert mapped.status_code == HTTP_OK, mapped.text
|
||||
body = mapped.json()
|
||||
assert body["row"]["counterparty_type"] == "GSK"
|
||||
assert body["propagated"] >= 1
|
||||
|
||||
after = await _get(client, f"{version_url}/osv/summary")
|
||||
rows_after = await _all_rows(client, version_url)
|
||||
# `propagated` считает записи `osv_entry` (в том числе в парах-пробелах 66/76, строк в своде не дающих), а
|
||||
# `unmapped_total` — строки свода: квартальные записи одного контрагента склеены (D-2). Поэтому `unmapped_total`
|
||||
# падает на число склеенных строк свода, получивших контрагента, а «1 + propagated» сверяется с записями в БД
|
||||
# (design #79).
|
||||
before_by_entries = {entry_id: row for row in rows for entry_id in row["entry_ids"]}
|
||||
changed = {
|
||||
tuple(row["entry_ids"]) for row in rows_after
|
||||
if row["counterparty_id"] is not None and before_by_entries[row["entry_ids"][0]]["counterparty_id"] is None
|
||||
} # fmt: skip
|
||||
assert len(changed) >= 2 # noqa: PLR2004 - счета 60 и 62
|
||||
stored = (
|
||||
await seeded.execute(
|
||||
select(OsvEntry).where(OsvEntry.owner_legal_entity_id == rb.id, OsvEntry.counterparty_name.is_not(None))
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
).scalars().all() # fmt: skip
|
||||
named = [entry for entry in stored if _is_schastl(entry.counterparty_name)]
|
||||
assert len(named) == 1 + body["propagated"]
|
||||
assert {entry.counterparty_id for entry in named} == {schastl.id}
|
||||
assert {entry.counterparty_match_kind for entry in named} == {
|
||||
CounterpartyMatchKind.MANUAL,
|
||||
CounterpartyMatchKind.PROPAGATED,
|
||||
}
|
||||
assert {entry.account_root for entry in named} >= {"60", "62", "66", "76"} # включая пары-пробелы
|
||||
assert after["unmapped_total"] == unmapped_before - len(changed)
|
||||
|
||||
same_name = [r for r in rows_after if r["legal_entity_id"] == str(rb.id) and _is_schastl(r["counterparty_name"])]
|
||||
assert {r["account_root"] for r in same_name} >= {"60", "62"} # 66 и 76 РБ — пробелы, строк в своде нет
|
||||
assert all(r["counterparty_id"] == str(schastl.id) and r["counterparty_type"] == "GSK" for r in same_name)
|
||||
|
||||
# Счастливочная: контрагент «Русский бисквит» с ИНН — GSK по ИНН
|
||||
inn_rows = [
|
||||
r
|
||||
for r in rows_after
|
||||
if r["legal_entity_id"] == str(schastl.id)
|
||||
and r["counterparty_name"] == RB_NAME_IN_SCHASTL
|
||||
and r["counterparty_inn"] == RB_INN
|
||||
]
|
||||
assert inn_rows
|
||||
assert all(r["counterparty_type"] == "GSK" and r["match_kind"] == "INN" for r in inn_rows)
|
||||
|
||||
# сверка суммы периодов с golden: контрагент РБ по счёту 60, суммы Q1+Q2+Q3 считаются здесь, а не доменом
|
||||
golden = _golden_rows(fixtures_dir, "rb", "60")
|
||||
keys = {q: defaultdict(list) for q in golden}
|
||||
for quarter, quarter_rows in golden.items():
|
||||
for row in quarter_rows:
|
||||
keys[quarter][_key(row)].append(row)
|
||||
common = [
|
||||
k for k in keys[Q1] if k in keys[Q2] and k in keys[Q3] and any(_dec(r["turnover_debit"]) for r in keys[Q2][k])
|
||||
]
|
||||
assert common
|
||||
checked = 0
|
||||
for key in common[:5]:
|
||||
account_code, name, inn = key
|
||||
found = [
|
||||
r for r in rows_after
|
||||
if r["legal_entity_id"] == str(rb.id) and r["account_code"] == account_code
|
||||
and (r["counterparty_name"] or "").strip() == name and (r["counterparty_inn"] or "").replace(" ", "") == inn
|
||||
] # fmt: skip
|
||||
assert len(found) == 1, key
|
||||
for field in SUMS:
|
||||
first_q, last_q = keys[Q1][key], keys[Q3][key]
|
||||
if field.startswith("opening"):
|
||||
expected = sum(_dec(r[field]) for r in first_q)
|
||||
elif field.startswith("closing"):
|
||||
expected = sum(_dec(r[field]) for r in last_q)
|
||||
else:
|
||||
expected = sum(_dec(r[field]) for q in (Q1, Q2, Q3) for r in keys[q][key])
|
||||
assert Decimal(found[0][field]) == expected, (key, field)
|
||||
assert found[0]["is_cumulative"] is False and len(found[0]["entry_ids"]) >= 3 # noqa: PLR2004
|
||||
checked += 1
|
||||
assert checked > 0
|
||||
|
||||
# экспорт
|
||||
export = await client.get(f"{version_url}/osv/export")
|
||||
assert export.status_code == HTTP_OK, export.text
|
||||
book = load_workbook(io.BytesIO(export.content))
|
||||
main = list(book[book.sheetnames[0]].iter_rows(values_only=True))
|
||||
assert len(main) - 1 == after["rows_total"]
|
||||
|
||||
elapsed = time.perf_counter() - started
|
||||
logger.info("сквозной сценарий РБ: {:.1f} с, строк свода {}", elapsed, len(rows_after))
|
||||
assert elapsed <= TIME_BUDGET_SECONDS
|
||||
assert all(d.status is not DocumentStatus.NEEDS_CHECK for d in documents)
|
||||
@ -34,6 +34,7 @@ from src.models.osv import OsvEntry
|
||||
from src.repositories.counterparties import CounterpartyIndex
|
||||
from src.services.result_mapping import map_file
|
||||
from tests.factories import make_legal_entity, make_stored_file
|
||||
from tests.fixtures_helpers import golden_result
|
||||
|
||||
GOLDEN_DIR = Path("golden") / "expected_osv_parse"
|
||||
EXPECTED_SNAPSHOTS = 82 # результатов ОСВ в fixtures (rb, schastl, synthetic)
|
||||
@ -48,13 +49,6 @@ def _snapshots(fixtures_dir: Path) -> list[Path]:
|
||||
return sorted((fixtures_dir / GOLDEN_DIR).rglob("*.json"))
|
||||
|
||||
|
||||
def _payload(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> RecognitionResult:
|
||||
data = json.loads(path.read_text("utf-8"))
|
||||
# у снимка нет job_id/file_id (подставляются) и версии сервиса (в контракте она обязательна)
|
||||
data["service"] = {"version": "golden", **data["service"]}
|
||||
return RecognitionResult.model_validate(data | {"job_id": str(job_id), "file_id": str(file_id)})
|
||||
|
||||
|
||||
def _decimal(value: str | None) -> Decimal | None:
|
||||
return Decimal(value) if value is not None else None
|
||||
|
||||
@ -116,7 +110,7 @@ async def test_all_golden_snapshots_mapped(seeded: AsyncSession, fixtures_dir: P
|
||||
job_status=RecognitionStatus.done,
|
||||
original_name=path.name,
|
||||
)
|
||||
result = _payload(path, job_id=job.id, file_id=file.id)
|
||||
result = golden_result(path, job_id=job.id, file_id=file.id)
|
||||
started = time.perf_counter()
|
||||
await map_file(session, job=job, file=file, result=result, chart=chart, index=index, now=datetime.now(UTC))
|
||||
mapping_seconds += time.perf_counter() - started
|
||||
|
||||
@ -252,9 +252,9 @@
|
||||
- Сценарий: seed ГК «Русский бисквит» (СПб РФ) с ЮЛ АО РБ (ИНН 3528052718) и ООО «Пекарня Счастливочная» (ИНН из `fixtures/directories/ecp-export-russkiy-biskvit.xlsx`, см. `test_import_full_export`) и ГСК из них; маппинг golden `rb/2024-Q1..Q3/*.json` и `schastl/2024-Q1..Q3/*.json` через `map_file` (реальный `CounterpartyIndex`); подтверждение всех документов через `POST /documents/{id}/confirm` (`ka_rf`); `POST /consolidations` 2024 Q3 → `INITIATED` (подхват) → `POST …/lock` → `GET …/osv/summary`: строки есть по счетам 60 и 62 у обеих и 76 у Счастливочной; `gaps` содержат РБ 66/67/76 с `missing_quarters ⊇ [Q1, Q2]` (и Счастливочной 01/02); ячейка чек-листа ОСВ 58 РБ — не `CONFIRMED` (есть только 58.3); `GET …/osv?unmapped=true` содержит строки РБ «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» (имя как в файле, без учёта регистра — найти по `normalize_alias`); `POST …/osv/entries/{id}/counterparty` → `GSK`, `propagated ≥ 1`; повторный `summary.unmapped_total` уменьшился на `1 + propagated`; все строки с этим именем во всех счетах РБ — `GSK`; строка Счастливочной «Русский бисквит» с ИНН — `GSK`, `match_kind = INN`; `GET …/osv/export` открывается `openpyxl`, число строк = `rows_total`.
|
||||
- Время теста ≤ 60 с; выводить в лог число строк свода.
|
||||
|
||||
- [ ] **Step 1: Тест** по сценарию (сначала падает на отсутствии хелпера или данных).
|
||||
- [x] **Step 1: Тест** по сценарию (сначала падает на отсутствии хелпера или данных).
|
||||
- [ ] **Step 2–4:** хелпер → `pytest -m "integration and fixtures" tests/test_consolidation_fixtures.py -q` → PASS; `make check-full` (без `e2e`, если compose с парсером не поднят — отметить в ledger).
|
||||
- [ ] **Step 5: Commit** `test(consolidation): сквозной сценарий Среза 4 на фикстурах группы «Русский бисквит»`.
|
||||
- [x] **Step 5: Commit** `test(consolidation): сквозной сценарий Среза 4 на фикстурах группы «Русский бисквит»` (тест под `xfail(strict)`: найден дефект привязки Q1, см. `task-13-report.md`; Step 2–4 закрыть после исправления).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -263,3 +263,5 @@ legal_entity_name, account_code, missing_quarters[]}}`. Фильтры `GET …/
|
||||
| 76 | 2026-10-06 | Аудит `EXPORT` пишется после сборки книги в той же транзакции запроса: `filters = f.model_dump(mode="json", exclude_none=True)` (enum → значение, UUID → строка; пустые фильтры — `{}`), `rows` — число строк листа после фильтров; ошибка сборки откатывает и аудит; отказы (403/404) не аудируются | spec audit-trail: EXPORT — действие над версией; запись без выгруженного файла была бы ложной | при сбое после коммита (обрыв соединения при отдаче) событие остаётся, а файл клиент мог не получить; `model_dump(mode="json", exclude_none=True)` отличается от буквального `model_dump` плана (M-5): UUID и enum должны быть JSON-сериализуемы для `audit_event` |
|
||||
| 77 | 2026-10-06 | R-12: все текстовые ячейки экспорта (компания, ИНН, счёт, период, контрагент, ИНН контрагента, «Сопоставлено с», тип, способ, лист «Пробелы») проходят `_put_text`: запрещённые XML-символы (`ILLEGAL_CHARACTERS_RE`) вырезаются, тип ячейки принудительно `"s"`, поэтому `=`, `+`, `-`, `@` в начале не становятся формулой, а текст читается обратно без изменений | файлы внешние, аналитики открывают выгрузку в Excel: без этого `=HYPERLINK(...)` из чужого файла выполнился бы в их книге, а управляющий символ давал `IllegalCharacterError` → 500 | текст ячейки с ведущим `=` больше не вычисляется — это желаемо; вырезанный управляющий символ из выгрузки пропадает |
|
||||
| 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 вводит в заблуждение для квартальных документов; уточнить при архивации (не меняет код) |
|
||||
|
||||
@ -59,6 +59,8 @@
|
||||
- [ ] 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 после исправления.
|
||||
- [ ] 5.2 **Документы.** README (консолидации, статусы Среза 4, свод, сопоставление, экспорт, коды ошибок), CLAUDE.md
|
||||
(новые модули, порядок блокировок), `openspec` спеки; синхронизация с docs-change (OQ, §2/§3/§9).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user