feat(#195): counter_pairs в сводке сводной ОСВ и фильтр counterparty_id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
40c1bca6bc
commit
c24bc3d4c1
@ -33,7 +33,8 @@ async def list_osv(
|
||||
query: Annotated[OsvQuery, Query()],
|
||||
) -> Page[ConsolidatedRowOut]:
|
||||
"""Строки свода в порядке: имя ЮЛ, счёт, номер строки, первый `entry_id`. Фильтры `legal_entity_id`,
|
||||
`account_root`, `counterparty_type`, `unmapped`, `match_kind` (повторяемый), `q`; `limit` ≤ 2000 (по умолчанию 500).
|
||||
`account_root`, `counterparty_type`, `unmapped`, `counterparty_id`, `match_kind` (повторяемый), `q`;
|
||||
`limit` ≤ 2000 (по умолчанию 500).
|
||||
Суммы — строки с двумя знаками."""
|
||||
return await service.page(version_id, query, query)
|
||||
|
||||
|
||||
@ -241,6 +241,18 @@ class GapOut(BaseModel):
|
||||
missing_quarters: list[PeriodQuarter]
|
||||
|
||||
|
||||
class CounterPairOut(BaseModel):
|
||||
"""Пара «держатель видит, встречной записи нет» (§3 п. 9, AURORA-1746) в сводке свода."""
|
||||
|
||||
holder_id: uuid.UUID
|
||||
holder_name: str
|
||||
missing_id: uuid.UUID
|
||||
missing_name: str
|
||||
holder_rows: int
|
||||
missing_unmapped_rows: int
|
||||
missing_rows_total: int
|
||||
|
||||
|
||||
class OsvSummaryOut(BaseModel):
|
||||
"""Сводка по своду версии (`GET …/osv/summary`)."""
|
||||
|
||||
@ -249,6 +261,7 @@ class OsvSummaryOut(BaseModel):
|
||||
auto_matched_total: int
|
||||
documents_total: int
|
||||
gaps: list[GapOut]
|
||||
counter_pairs: list[CounterPairOut]
|
||||
|
||||
|
||||
class OsvFilters(BaseModel):
|
||||
@ -258,6 +271,7 @@ class OsvFilters(BaseModel):
|
||||
account_root: str | None = None
|
||||
counterparty_type: CounterpartyType | None = None
|
||||
unmapped: bool | None = None
|
||||
counterparty_id: uuid.UUID | None = None
|
||||
match_kind: list[CounterpartyMatchKind] | None = None
|
||||
q: str | None = Field(default=None, max_length=MAX_OSV_QUERY)
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ from src.schemas.common import Page
|
||||
from src.schemas.consolidations import (
|
||||
CandidateOut,
|
||||
ConsolidatedRowOut,
|
||||
CounterPairOut,
|
||||
GapOut,
|
||||
OsvFilters,
|
||||
OsvPageParams,
|
||||
@ -65,6 +66,7 @@ def domain_filter(f: OsvFilters) -> domain.OsvFilter:
|
||||
account_root=f.account_root,
|
||||
counterparty_type=f.counterparty_type,
|
||||
unmapped=f.unmapped,
|
||||
counterparty_id=f.counterparty_id,
|
||||
match_kinds=frozenset(f.match_kind or ()),
|
||||
q=f.q,
|
||||
)
|
||||
@ -232,6 +234,10 @@ class ConsolidatedOsvService:
|
||||
auto_matched_total=auto_matched_total,
|
||||
documents_total=result.consolidated.documents_total,
|
||||
gaps=[GapOut.model_validate(gap, from_attributes=True) for gap in result.consolidated.gaps],
|
||||
counter_pairs=[
|
||||
CounterPairOut.model_validate(pair, from_attributes=True)
|
||||
for pair in domain.counter_pairs(result.consolidated.rows, result.members)
|
||||
],
|
||||
)
|
||||
|
||||
async def row_for_entry(self, version_id: uuid.UUID, entry_id: uuid.UUID) -> ConsolidatedRowOut | None:
|
||||
|
||||
@ -277,7 +277,7 @@ async def test_summary_counts_and_page_total_agree(auth_client: AuthClientFactor
|
||||
|
||||
assert summary == {
|
||||
"rows_total": ROWS_TOTAL, "unmapped_total": UNMAPPED_TOTAL, "auto_matched_total": AUTO_MATCHED_TOTAL,
|
||||
"documents_total": DOCUMENTS_TOTAL, "gaps": [],
|
||||
"documents_total": DOCUMENTS_TOTAL, "gaps": [], "counter_pairs": [],
|
||||
} # fmt: skip
|
||||
unmapped = await _page(client, world, "unmapped=true")
|
||||
assert unmapped["total"] == summary["unmapped_total"]
|
||||
@ -287,6 +287,22 @@ async def test_summary_counts_and_page_total_agree(auth_client: AuthClientFactor
|
||||
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)
|
||||
|
||||
@ -902,3 +902,32 @@ async def test_alias_delete_racing_with_move_alias_is_404_without_phantom_event(
|
||||
]
|
||||
assert len(removals) == 1 # только удаление при переносе
|
||||
assert await session.get(LegalEntityAlias, alias_id) is None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_counter_pairs_before_and_after_mapping(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||
"""Пара «Счастливочная видит РБ, РБ её — нет»: у РБ без ИНН 60/62/66, 76 с ИНН вне справочника (AURORA-1866)."""
|
||||
world = await _world(seeded)
|
||||
client = await auth_client(KA_RF)
|
||||
base = f"/consolidations/versions/{world.version.id}/osv"
|
||||
|
||||
summary = await _summary(client, world)
|
||||
|
||||
assert summary["counter_pairs"] == [
|
||||
{
|
||||
"holder_id": str(world.schastl.id), "holder_name": "ООО «Пекарня Счастливочная»",
|
||||
"missing_id": str(world.rb.id), "missing_name": "АО Русский бисквит",
|
||||
"holder_rows": 1, "missing_unmapped_rows": 3, "missing_rows_total": 5,
|
||||
}
|
||||
] # fmt: skip
|
||||
unmapped = await client.get(f"{base}?legal_entity_id={world.rb.id}&unmapped=true")
|
||||
assert unmapped.json()["total"] == 3 # noqa: PLR2004
|
||||
held = await client.get(f"{base}?legal_entity_id={world.schastl.id}&counterparty_id={world.rb.id}")
|
||||
assert held.json()["total"] == 1
|
||||
|
||||
mapped = await client.post(
|
||||
_map_url(world, world.entries[("60", 1)]), json={"legal_entity_id": str(world.schastl.id)}
|
||||
)
|
||||
|
||||
assert mapped.status_code == HTTP_OK, mapped.text
|
||||
assert (await _summary(client, world))["counter_pairs"] == []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user