478 lines
23 KiB
Python
478 lines
23 KiB
Python
"""API сводной ОСВ версии (spec consolidated-osv «Страница свода и сводка»; design D-1, D-16): страница, фильтры,
|
||
сводка с пробелами, суммы-строки, права и видимость, производительность без N+1."""
|
||
|
||
import time
|
||
import uuid
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from sqlalchemy import event, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from src.domain.aliases import normalize_alias
|
||
from src.models.access import AppUser
|
||
from src.models.consolidation import ConsolidationDocument, ConsolidationVersion
|
||
from src.models.directories import CompanyGroup, LegalEntity, LegalEntityAlias, RegionalOffice
|
||
from src.models.enums import ConsolidationStatus, CounterpartyMatchKind, PeriodQuarter
|
||
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_confirmed_osv, make_consolidation, make_legal_entity
|
||
|
||
HTTP_OK = 200
|
||
HTTP_FORBIDDEN = 403
|
||
HTTP_NOT_FOUND = 404
|
||
HTTP_UNPROCESSABLE = 422
|
||
KA_RF = "ka_rf@armka.local"
|
||
CURATOR = "curator_go@armka.local"
|
||
FOREIGN = "client_manager@armka.local" # Краснодарский филиал
|
||
ROSTOV = "Ростовский региональный филиал"
|
||
YEAR = 2024
|
||
Q1, Q2, Q3 = PeriodQuarter.Q1, PeriodQuarter.Q2, PeriodQuarter.Q3
|
||
D = Decimal
|
||
ZERO = D("0.00")
|
||
ROWS_TOTAL, UNMAPPED_TOTAL, AUTO_MATCHED_TOTAL, DOCUMENTS_TOTAL = 8, 2, 2, 7
|
||
PERF_MAX_SECONDS = 5.0
|
||
PERF_MAX_SELECTS = 10
|
||
|
||
|
||
class World:
|
||
"""Версия РБ Q3 2024: периметр {А, Б}; вне периметра — ЧКФ (та же ГК), «Внешний» (имя), Ф (алиас)."""
|
||
|
||
version: ConsolidationVersion
|
||
a: LegalEntity
|
||
b: LegalEntity
|
||
chkf: LegalEntity
|
||
external: LegalEntity
|
||
aliased: LegalEntity
|
||
|
||
|
||
def _quarter_rows(world: World, quarter: PeriodQuarter) -> list[dict[str, Any]]:
|
||
"""Строки ОСВ 60 «А»: opening/turnover/closing по кварталам 100/10/110, 110/10/120, 120/10/130."""
|
||
step = {Q1: 0, Q2: 10, Q3: 20}[quarter]
|
||
amounts = {
|
||
"opening_debit": D(100 + step),
|
||
"turnover_debit": D(10),
|
||
"closing_debit": D(110 + step),
|
||
}
|
||
return [
|
||
{
|
||
"name": "ООО Б",
|
||
"inn": world.b.inn,
|
||
"counterparty_id": world.b.id,
|
||
"counterparty_match_kind": CounterpartyMatchKind.INN,
|
||
**amounts,
|
||
},
|
||
{
|
||
"name": "ООО ЧКФ",
|
||
"counterparty_id": world.chkf.id,
|
||
"counterparty_match_kind": CounterpartyMatchKind.MANUAL,
|
||
**amounts,
|
||
},
|
||
{"name": "Неизвестный контрагент", **amounts},
|
||
{"name": "ООО Внешний", **amounts},
|
||
{"name": "Пекарня Ф", **amounts},
|
||
]
|
||
|
||
|
||
async def _bind(session: AsyncSession, version: ConsolidationVersion, *documents: Any) -> None:
|
||
for document in documents:
|
||
session.add(ConsolidationDocument(consolidation_version_id=version.id, document_id=document.id))
|
||
await session.flush()
|
||
|
||
|
||
async def _world(session: AsyncSession, *, status: ConsolidationStatus = ConsolidationStatus.DATA_CORRECTION) -> World:
|
||
office = (await session.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
|
||
group: CompanyGroup = await make_company_group(session, name="ГК Русский бисквит", office=office)
|
||
world = World()
|
||
world.a = await make_legal_entity(session, name="ООО А", group=group)
|
||
world.b = await make_legal_entity(session, name="ООО Б", group=group)
|
||
world.chkf = await make_legal_entity(session, name="ООО ЧКФ", group=group)
|
||
world.external = await make_legal_entity(session, name="ООО Внешний")
|
||
world.aliased = await make_legal_entity(session, name="Фирма Эф")
|
||
session.add(
|
||
LegalEntityAlias(
|
||
legal_entity_id=world.aliased.id, alias="Пекарня Ф", normalized_alias=normalize_alias("Пекарня Ф")
|
||
)
|
||
)
|
||
user = (await session.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
|
||
_, world.version = await make_consolidation(
|
||
session, group=group, year=YEAR, quarter=Q3, members=[world.a, world.b], initiator=user, status=status
|
||
)
|
||
documents = []
|
||
for quarter in (Q1, Q2, Q3):
|
||
documents.append(
|
||
await make_confirmed_osv(
|
||
session,
|
||
entity=world.a,
|
||
account="60",
|
||
year=YEAR,
|
||
quarter=quarter,
|
||
is_cumulative=False,
|
||
rows=_quarter_rows(world, quarter),
|
||
)
|
||
)
|
||
documents.append(
|
||
await make_confirmed_osv(
|
||
session,
|
||
entity=world.b,
|
||
account="60",
|
||
year=YEAR,
|
||
quarter=quarter,
|
||
is_cumulative=False,
|
||
rows=[
|
||
{
|
||
"name": "ООО А",
|
||
"counterparty_id": world.a.id,
|
||
"counterparty_match_kind": CounterpartyMatchKind.MANUAL,
|
||
"turnover_credit": D("5.00"),
|
||
},
|
||
{"name": "Неизвестный Б", "turnover_credit": D("1.00")},
|
||
],
|
||
)
|
||
)
|
||
documents.append(
|
||
await make_confirmed_osv(
|
||
session,
|
||
entity=world.a,
|
||
account="62",
|
||
year=YEAR,
|
||
quarter=Q3,
|
||
is_cumulative=True,
|
||
rows=[
|
||
{
|
||
"name": "ООО Б",
|
||
"inn": world.b.inn,
|
||
"counterparty_id": world.b.id,
|
||
"counterparty_match_kind": CounterpartyMatchKind.INN,
|
||
"closing_credit": D("42.50"),
|
||
}
|
||
],
|
||
)
|
||
)
|
||
await _bind(session, world.version, *documents)
|
||
return world
|
||
|
||
|
||
def _url(world: World, suffix: str = "osv") -> str:
|
||
return f"/consolidations/versions/{world.version.id}/{suffix}"
|
||
|
||
|
||
async def _page(client: Any, world: World, query: str = "") -> dict[str, Any]:
|
||
response = await client.get(f"{_url(world)}?{query}")
|
||
assert response.status_code == HTTP_OK, response.text
|
||
body: dict[str, Any] = response.json()
|
||
return body
|
||
|
||
|
||
def _row(body: dict[str, Any], entity: str, account: str, name: str) -> dict[str, Any]:
|
||
return next(
|
||
row
|
||
for row in body["items"]
|
||
if (row["legal_entity_name"], row["account_code"], row["counterparty_name"]) == (entity, account, name)
|
||
)
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_page_rows_order_total_and_money_strings(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
body = await _page(client, world)
|
||
|
||
assert (body["total"], body["limit"], body["offset"]) == (ROWS_TOTAL, 500, 0)
|
||
assert [(r["legal_entity_name"], r["account_code"], r["row_number"]) for r in body["items"]] == [
|
||
("ООО А", "60", 1), ("ООО А", "60", 2), ("ООО А", "60", 3), ("ООО А", "60", 4), ("ООО А", "60", 5),
|
||
("ООО А", "62", 1), ("ООО Б", "60", 1), ("ООО Б", "60", 2),
|
||
] # fmt: skip
|
||
row = _row(body, "ООО А", "60", "ООО Б")
|
||
assert row["period"] == "2024 Q3" and row["is_cumulative"] is False
|
||
assert (row["opening_debit"], row["turnover_debit"], row["closing_debit"]) == ("100.00", "30.00", "130.00")
|
||
assert (row["opening_credit"], row["turnover_credit"], row["closing_credit"]) == ("0.00", "0.00", "0.00")
|
||
assert len(row["entry_ids"]) == 3 # noqa: PLR2004
|
||
assert all(isinstance(entry_id, str) for entry_id in row["entry_ids"])
|
||
assert row["warnings"] == []
|
||
assert row["legal_entity_id"] == str(world.a.id) and row["legal_entity_inn"] == world.a.inn
|
||
assert row["account_root"] == "60" and row["counterparty_inn"] == world.b.inn
|
||
assert (row["counterparty_id"], row["counterparty_entity_name"]) == (str(world.b.id), "ООО Б")
|
||
assert row["counterparty_entity_inn"] == world.b.inn
|
||
assert (row["counterparty_type"], row["match_kind"], row["match_stored"]) == ("GSK", "INN", True)
|
||
cumulative = _row(body, "ООО А", "62", "ООО Б")
|
||
assert cumulative["is_cumulative"] is True and cumulative["closing_credit"] == "42.50"
|
||
assert len(cumulative["entry_ids"]) == 1
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_counterparty_types_and_read_time_resolution(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||
) -> None:
|
||
world = await _world(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
body = await _page(client, world)
|
||
|
||
chkf = _row(body, "ООО А", "60", "ООО ЧКФ")
|
||
assert (chkf["counterparty_type"], chkf["match_kind"], chkf["match_stored"]) == ("GK", "MANUAL", True)
|
||
unknown = _row(body, "ООО А", "60", "Неизвестный контрагент")
|
||
assert (unknown["counterparty_id"], unknown["counterparty_type"], unknown["match_kind"]) == (None, "EXTERNAL", None)
|
||
assert unknown["match_stored"] is False and unknown["counterparty_entity_name"] is None
|
||
assert unknown["counterparty_entity_inn"] is None
|
||
by_name = _row(body, "ООО А", "60", "ООО Внешний")
|
||
assert (by_name["counterparty_id"], by_name["match_kind"], by_name["match_stored"]) == (
|
||
str(world.external.id), "NAME", False) # fmt: skip
|
||
assert by_name["counterparty_type"] == "EXTERNAL"
|
||
by_alias = _row(body, "ООО А", "60", "Пекарня Ф")
|
||
assert (by_alias["counterparty_id"], by_alias["match_kind"], by_alias["match_stored"]) == (
|
||
str(world.aliased.id), "ALIAS", False) # fmt: skip
|
||
assert by_alias["counterparty_entity_name"] == "Фирма Эф"
|
||
# ИНН в файле нет — ИНН сопоставленного ЮЛ справочника отдаётся отдельным полем (arm-core #74)
|
||
assert (by_alias["counterparty_inn"], by_alias["counterparty_entity_inn"]) == (None, world.aliased.inn)
|
||
assert _row(body, "ООО Б", "60", "ООО А")["counterparty_type"] == "GSK"
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_read_time_resolution_alias_wins_over_namesakes(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||
) -> None:
|
||
"""OQ-128 (§3 п. 4, п. 7): после ручного сопоставления алиас приоритетнее ЮЛ-тёзок по нормализованному имени —
|
||
дорезолв на чтение отдаёт владельца алиаса (`ALIAS`, `match_stored = false`), а не «несопоставлено»."""
|
||
world = await _world(seeded)
|
||
await make_legal_entity(seeded, name="ООО Мю Трейд", inn="6601000001")
|
||
await make_legal_entity(seeded, name="АО Мю Трейд", inn="6601000002")
|
||
owner = await make_legal_entity(seeded, name="ООО Совсем Другое Имя", inn="6601000003")
|
||
seeded.add(
|
||
LegalEntityAlias(legal_entity_id=owner.id, alias="Мю Трейд", normalized_alias=normalize_alias("Мю Трейд"))
|
||
)
|
||
document = await make_confirmed_osv(
|
||
seeded,
|
||
entity=world.a,
|
||
account="70",
|
||
year=YEAR,
|
||
quarter=Q3,
|
||
is_cumulative=True,
|
||
rows=[{"name": "Мю Трейд", "closing_debit": D(1)}, {"name": "Ню Трейд", "closing_debit": D(1)}],
|
||
)
|
||
await _bind(seeded, world.version, document)
|
||
await make_legal_entity(seeded, name="ООО Ню Трейд", inn="6601000004")
|
||
await make_legal_entity(seeded, name="АО Ню Трейд", inn="6601000005")
|
||
client = await auth_client(KA_RF)
|
||
|
||
body = await _page(client, world)
|
||
|
||
resolved = _row(body, "ООО А", "70", "Мю Трейд")
|
||
assert (resolved["counterparty_id"], resolved["match_kind"], resolved["match_stored"]) == (
|
||
str(owner.id), "ALIAS", False) # fmt: skip
|
||
# тёзки без алиаса по-прежнему неоднозначны
|
||
ambiguous = _row(body, "ООО А", "70", "Ню Трейд")
|
||
assert (ambiguous["counterparty_id"], ambiguous["match_kind"]) == (None, None)
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_summary_counts_and_page_total_agree(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
summary = (await client.get(_url(world, "osv/summary"))).json()
|
||
|
||
assert summary == {
|
||
"rows_total": ROWS_TOTAL, "unmapped_total": UNMAPPED_TOTAL, "auto_matched_total": AUTO_MATCHED_TOTAL,
|
||
"documents_total": DOCUMENTS_TOTAL, "gaps": [], "counter_pairs": [],
|
||
} # fmt: skip
|
||
unmapped = await _page(client, world, "unmapped=true")
|
||
assert unmapped["total"] == summary["unmapped_total"]
|
||
assert len(unmapped["items"]) == UNMAPPED_TOTAL
|
||
assert all(row["counterparty_id"] is None and row["counterparty_inn"] is None for row in unmapped["items"])
|
||
mapped = await _page(client, world, "unmapped=false")
|
||
assert mapped["total"] == ROWS_TOTAL - UNMAPPED_TOTAL
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_filter_by_counterparty_id(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
body = await _page(client, world, f"legal_entity_id={world.a.id}&counterparty_id={world.b.id}")
|
||
|
||
# у «ООО А» контрагент Б — в ОСВ 60 (склейка кварталов) и в ОСВ 62
|
||
assert body["total"] == 2 # noqa: PLR2004
|
||
assert {(r["legal_entity_name"], r["account_code"], r["counterparty_name"]) for r in body["items"]} == {
|
||
("ООО А", "60", "ООО Б"),
|
||
("ООО А", "62", "ООО Б"),
|
||
}
|
||
assert (await client.get(f"{_url(world)}?counterparty_id=not-a-uuid")).status_code == 422 # noqa: PLR2004
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_filter_by_match_kind_repeated_param(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
body = await _page(client, world, "match_kind=NAME&match_kind=ALIAS")
|
||
summary = (await client.get(_url(world, "osv/summary"))).json()
|
||
|
||
assert body["total"] == summary["auto_matched_total"] == AUTO_MATCHED_TOTAL
|
||
assert {row["match_kind"] for row in body["items"]} == {"NAME", "ALIAS"}
|
||
assert (await _page(client, world, "match_kind=MANUAL"))["total"] == 2 # noqa: PLR2004 - ЧКФ и «ООО А» у Б
|
||
assert (await _page(client, world, "match_kind=INN"))["total"] == 2 # noqa: PLR2004 - 60 и 62
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_filters_q_entity_account_root_type(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
by_name = await _page(client, world, "q=%D0%B2%D0%BD%D0%B5%D1%88") # «внеш» — регистр не важен
|
||
assert [row["counterparty_name"] for row in by_name["items"]] == ["ООО Внешний"]
|
||
by_inn = await _page(client, world, f"q={world.b.inn[:6]}") # префикс ИНН контрагента
|
||
assert {row["counterparty_name"] for row in by_inn["items"]} == {"ООО Б"} and by_inn["total"] == 2 # noqa: PLR2004
|
||
assert (await _page(client, world, f"legal_entity_id={world.b.id}"))["total"] == 2 # noqa: PLR2004
|
||
assert (await _page(client, world, "account_root=62"))["total"] == 1
|
||
assert (await _page(client, world, "counterparty_type=GK"))["total"] == 1
|
||
assert (await _page(client, world, "counterparty_type=GSK"))["total"] == 3 # noqa: PLR2004
|
||
both = await _page(client, world, f"legal_entity_id={world.a.id}&account_root=60&counterparty_type=EXTERNAL")
|
||
assert both["total"] == 3 # noqa: PLR2004 - неизвестный, «Внешний», «Пекарня Ф»
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_pagination_window_and_limit_bounds(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
body = await _page(client, world, "limit=2&offset=4")
|
||
|
||
assert (body["total"], body["limit"], body["offset"], len(body["items"])) == (ROWS_TOTAL, 2, 4, 2)
|
||
assert [row["row_number"] for row in body["items"]] == [5, 1] # пятая строка 60 «А», затем 62 «А»
|
||
assert (await _page(client, world, "limit=2000"))["limit"] == 2000 # noqa: PLR2004
|
||
assert (await _page(client, world, f"offset={ROWS_TOTAL}"))["items"] == []
|
||
for bad in ("limit=2001", "limit=0", "offset=-1", "counterparty_type=OTHER", "match_kind=BOGUS"):
|
||
assert (await client.get(f"{_url(world)}?{bad}")).status_code == HTTP_UNPROCESSABLE, bad
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_gap_is_reported_and_has_no_rows(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded)
|
||
# у «Б» отвязываем ОСВ 60 за Q2: остаются Q1 и Q3 → пробел
|
||
link = (
|
||
await seeded.execute(
|
||
select(ConsolidationDocument)
|
||
.join(Document, Document.id == ConsolidationDocument.document_id)
|
||
.where(
|
||
ConsolidationDocument.consolidation_version_id == world.version.id,
|
||
Document.legal_entity_id == world.b.id,
|
||
Document.period_quarter == Q2,
|
||
)
|
||
)
|
||
).scalar_one()
|
||
await seeded.delete(link)
|
||
await seeded.flush()
|
||
client = await auth_client(KA_RF)
|
||
|
||
summary = (await client.get(_url(world, "osv/summary"))).json()
|
||
body = await _page(client, world, f"legal_entity_id={world.b.id}")
|
||
|
||
assert summary["gaps"] == [
|
||
{"legal_entity_id": str(world.b.id), "legal_entity_name": "ООО Б", "account_code": "60",
|
||
"missing_quarters": ["Q2"]}
|
||
] # fmt: skip
|
||
assert summary["documents_total"] == DOCUMENTS_TOTAL - 1
|
||
assert body["total"] == 0
|
||
assert summary["rows_total"] == ROWS_TOTAL - 2
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_cancelled_version_is_readable(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded, status=ConsolidationStatus.CANCELLED)
|
||
client = await auth_client(KA_RF)
|
||
|
||
assert (await _page(client, world))["total"] == ROWS_TOTAL
|
||
assert (await client.get(_url(world, "osv/summary"))).status_code == HTTP_OK
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_rights_and_visibility(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
world = await _world(seeded)
|
||
curator = await auth_client(CURATOR)
|
||
foreign = await auth_client(FOREIGN)
|
||
admin = await auth_client("admin@armka.local") # права CONSOLIDATION_VIEW у роли нет
|
||
|
||
assert (await curator.get(_url(world))).status_code == HTTP_OK
|
||
assert (await curator.get(_url(world, "osv/summary"))).status_code == HTTP_OK
|
||
for suffix in ("osv", "osv/summary"):
|
||
hidden = await foreign.get(_url(world, suffix))
|
||
unknown = await foreign.get(f"/consolidations/versions/{uuid.uuid4()}/{suffix}")
|
||
assert hidden.status_code == unknown.status_code == HTTP_NOT_FOUND
|
||
assert hidden.json() == unknown.json()
|
||
assert (await admin.get(_url(world, suffix))).status_code == HTTP_FORBIDDEN
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_read_is_side_effect_free(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
"""Дорезолв не пишется: `osv_entry.counterparty_id` строки «ООО Внешний» остаётся пустым."""
|
||
world = await _world(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
await _page(client, world)
|
||
|
||
resolved = (
|
||
await seeded.execute(
|
||
select(OsvEntry.counterparty_id).where(OsvEntry.counterparty_name.in_(["ООО Внешний", "Пекарня Ф"]))
|
||
)
|
||
).scalars()
|
||
assert set(resolved) == {None}
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_large_version_is_fast_without_n_plus_one(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
"""Свод из 5 400 исходных строк (2 ЮЛ × 3 счёта × 3 квартала × 300): ответ быстро, SELECT-ов — константа."""
|
||
office = (await seeded.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
|
||
group = await make_company_group(seeded, name="ГК Большая", office=office)
|
||
members = [await make_legal_entity(seeded, name=f"ООО Большая {n}", group=group) for n in range(2)]
|
||
counterparties = [await make_legal_entity(seeded, name=f"Контрагент {n:03d}", group=group) for n in range(50)]
|
||
user = (await seeded.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
|
||
_, version = await make_consolidation(
|
||
seeded, group=group, year=YEAR, quarter=Q3, members=members, initiator=user,
|
||
status=ConsolidationStatus.DATA_CORRECTION,
|
||
) # fmt: skip
|
||
documents = []
|
||
for entity in members:
|
||
for account in ("60", "62", "76"):
|
||
for quarter in (Q1, Q2, Q3):
|
||
rows = [
|
||
{
|
||
"name": f"Контрагент {n:03d}" if n % 2 else f"Неизвестный {n}",
|
||
"counterparty_id": counterparties[n % 50].id if n % 3 == 0 else None,
|
||
"counterparty_match_kind": CounterpartyMatchKind.MANUAL if n % 3 == 0 else None,
|
||
"opening_debit": D("100.00"),
|
||
"turnover_debit": D("10.00"),
|
||
"closing_debit": D("110.00"),
|
||
}
|
||
for n in range(300)
|
||
]
|
||
documents.append(
|
||
await make_confirmed_osv(
|
||
seeded, entity=entity, account=account, year=YEAR, quarter=quarter, is_cumulative=False,
|
||
rows=rows,
|
||
)
|
||
) # fmt: skip
|
||
await _bind(seeded, version, *documents)
|
||
client = await auth_client(KA_RF)
|
||
statements: list[str] = []
|
||
connection = (await seeded.connection()).sync_connection
|
||
listener = lambda _c, _cur, statement, *_rest: statements.append(statement) # noqa: E731
|
||
event.listen(connection, "before_cursor_execute", listener)
|
||
try:
|
||
started = time.perf_counter()
|
||
response = await client.get(f"/consolidations/versions/{version.id}/osv?limit=2000")
|
||
elapsed = time.perf_counter() - started
|
||
finally:
|
||
event.remove(connection, "before_cursor_execute", listener)
|
||
|
||
assert response.status_code == HTTP_OK, response.text
|
||
body = response.json()
|
||
assert body["total"] == 2 * 3 * 300 and len(body["items"]) == 1800 # noqa: PLR2004 - 5 400 исходных строк → 1 800
|
||
selects = [s for s in statements if s.lstrip().upper().startswith("SELECT")]
|
||
print(f"\nPERF osv: {elapsed:.3f}s, SELECT={len(selects)}, statements={len(statements)}") # noqa: T201
|
||
assert elapsed < PERF_MAX_SECONDS
|
||
assert len(selects) <= PERF_MAX_SELECTS, selects
|