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()],
|
query: Annotated[OsvQuery, Query()],
|
||||||
) -> Page[ConsolidatedRowOut]:
|
) -> Page[ConsolidatedRowOut]:
|
||||||
"""Строки свода в порядке: имя ЮЛ, счёт, номер строки, первый `entry_id`. Фильтры `legal_entity_id`,
|
"""Строки свода в порядке: имя ЮЛ, счёт, номер строки, первый `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)
|
return await service.page(version_id, query, query)
|
||||||
|
|
||||||
|
|||||||
@ -241,6 +241,18 @@ class GapOut(BaseModel):
|
|||||||
missing_quarters: list[PeriodQuarter]
|
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):
|
class OsvSummaryOut(BaseModel):
|
||||||
"""Сводка по своду версии (`GET …/osv/summary`)."""
|
"""Сводка по своду версии (`GET …/osv/summary`)."""
|
||||||
|
|
||||||
@ -249,6 +261,7 @@ class OsvSummaryOut(BaseModel):
|
|||||||
auto_matched_total: int
|
auto_matched_total: int
|
||||||
documents_total: int
|
documents_total: int
|
||||||
gaps: list[GapOut]
|
gaps: list[GapOut]
|
||||||
|
counter_pairs: list[CounterPairOut]
|
||||||
|
|
||||||
|
|
||||||
class OsvFilters(BaseModel):
|
class OsvFilters(BaseModel):
|
||||||
@ -258,6 +271,7 @@ class OsvFilters(BaseModel):
|
|||||||
account_root: str | None = None
|
account_root: str | None = None
|
||||||
counterparty_type: CounterpartyType | None = None
|
counterparty_type: CounterpartyType | None = None
|
||||||
unmapped: bool | None = None
|
unmapped: bool | None = None
|
||||||
|
counterparty_id: uuid.UUID | None = None
|
||||||
match_kind: list[CounterpartyMatchKind] | None = None
|
match_kind: list[CounterpartyMatchKind] | None = None
|
||||||
q: str | None = Field(default=None, max_length=MAX_OSV_QUERY)
|
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 (
|
from src.schemas.consolidations import (
|
||||||
CandidateOut,
|
CandidateOut,
|
||||||
ConsolidatedRowOut,
|
ConsolidatedRowOut,
|
||||||
|
CounterPairOut,
|
||||||
GapOut,
|
GapOut,
|
||||||
OsvFilters,
|
OsvFilters,
|
||||||
OsvPageParams,
|
OsvPageParams,
|
||||||
@ -65,6 +66,7 @@ def domain_filter(f: OsvFilters) -> domain.OsvFilter:
|
|||||||
account_root=f.account_root,
|
account_root=f.account_root,
|
||||||
counterparty_type=f.counterparty_type,
|
counterparty_type=f.counterparty_type,
|
||||||
unmapped=f.unmapped,
|
unmapped=f.unmapped,
|
||||||
|
counterparty_id=f.counterparty_id,
|
||||||
match_kinds=frozenset(f.match_kind or ()),
|
match_kinds=frozenset(f.match_kind or ()),
|
||||||
q=f.q,
|
q=f.q,
|
||||||
)
|
)
|
||||||
@ -232,6 +234,10 @@ class ConsolidatedOsvService:
|
|||||||
auto_matched_total=auto_matched_total,
|
auto_matched_total=auto_matched_total,
|
||||||
documents_total=result.consolidated.documents_total,
|
documents_total=result.consolidated.documents_total,
|
||||||
gaps=[GapOut.model_validate(gap, from_attributes=True) for gap in result.consolidated.gaps],
|
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:
|
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 == {
|
assert summary == {
|
||||||
"rows_total": ROWS_TOTAL, "unmapped_total": UNMAPPED_TOTAL, "auto_matched_total": AUTO_MATCHED_TOTAL,
|
"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
|
} # fmt: skip
|
||||||
unmapped = await _page(client, world, "unmapped=true")
|
unmapped = await _page(client, world, "unmapped=true")
|
||||||
assert unmapped["total"] == summary["unmapped_total"]
|
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
|
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
|
@pytest.mark.integration
|
||||||
async def test_filter_by_match_kind_repeated_param(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
async def test_filter_by_match_kind_repeated_param(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||||
world = await _world(seeded)
|
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 len(removals) == 1 # только удаление при переносе
|
||||||
assert await session.get(LegalEntityAlias, alias_id) is None
|
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