feat(consolidations): ячейки чек-листа с document_type и account своего правила (#42)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
61bbe78ce6
commit
678cc4566f
@ -99,8 +99,11 @@ class ConsolidationPeriodOut(BaseModel):
|
||||
|
||||
|
||||
class ChecklistCellOut(BaseModel):
|
||||
"""Ячейка чек-листа: статус пары (ЮЛ, правило), недостающие кварталы ОСВ и привязанные документы."""
|
||||
"""Ячейка чек-листа — самоописывающая (front-api-gaps D-2): правило ячейки (`document_type`, `account` — счёт ОСВ,
|
||||
`None` у ББ/ОФР), статус пары (ЮЛ, правило), недостающие кварталы ОСВ и привязанные документы."""
|
||||
|
||||
document_type: DocumentType
|
||||
account: str | None
|
||||
status: CellStatus # единственное определение — `domain.checklist.CellStatus` (T2.8)
|
||||
missing_quarters: list[PeriodQuarter]
|
||||
document_ids: list[uuid.UUID]
|
||||
|
||||
@ -192,7 +192,7 @@ class ConsolidationService:
|
||||
legal_entity_id=entity.id,
|
||||
legal_entity_name=entity.name,
|
||||
legal_entity_inn=entity.inn,
|
||||
cells=[self._cell_out(cell) for cell in cells],
|
||||
cells=[self._cell_out(rule, cell) for rule, cell in zip(rules, cells, strict=True)],
|
||||
)
|
||||
for entity, cells in rows
|
||||
],
|
||||
@ -236,9 +236,14 @@ class ConsolidationService:
|
||||
return rules, rows
|
||||
|
||||
@staticmethod
|
||||
def _cell_out(cell: checklist_domain.Cell) -> ChecklistCellOut:
|
||||
def _cell_out(rule: checklist_domain.Rule, cell: checklist_domain.Cell) -> ChecklistCellOut:
|
||||
"""Ячейка ответа с правилом (D-2); `rule` — правило того же столбца, что и `cell`."""
|
||||
return ChecklistCellOut(
|
||||
status=cell.status, missing_quarters=list(cell.missing_quarters), document_ids=list(cell.document_ids)
|
||||
document_type=rule.document_type,
|
||||
account=rule.account,
|
||||
status=cell.status,
|
||||
missing_quarters=list(cell.missing_quarters),
|
||||
document_ids=list(cell.document_ids),
|
||||
)
|
||||
|
||||
async def set_members(self, version_id: uuid.UUID, data: MembersIn) -> VersionCard:
|
||||
@ -385,7 +390,7 @@ class ConsolidationService:
|
||||
locks = await repo.active_locks(self._session, [version.id])
|
||||
history = await repo.history(self._session, version.id)
|
||||
brief = self._brief(version, locks.get(version.id), len(members))
|
||||
_, cells = await self._checklist_cells(version, consolidation, [entity for _, entity in members])
|
||||
rules, cells = await self._checklist_cells(version, consolidation, [entity for _, entity in members])
|
||||
return VersionCard(
|
||||
**brief.model_dump(),
|
||||
consolidation_id=consolidation.id,
|
||||
@ -404,7 +409,9 @@ class ConsolidationService:
|
||||
name=entity.name,
|
||||
inn=entity.inn,
|
||||
is_head_entity=member.is_head_entity,
|
||||
checklist=[self._cell_out(cell) for cell in cells[position][1]],
|
||||
checklist=[
|
||||
self._cell_out(rule, cell) for rule, cell in zip(rules, cells[position][1], strict=True)
|
||||
],
|
||||
)
|
||||
for position, (member, entity) in enumerate(members)
|
||||
],
|
||||
|
||||
@ -83,8 +83,20 @@ async def test_checklist_osv_60_confirmed_osv_90_missing(auth_client: AuthClient
|
||||
assert body["rows"][1]["legal_entity_inn"] == second.inn
|
||||
cells = _cells(body, "ООО А")
|
||||
assert len(cells) == len(EXPECTED_RULES)
|
||||
assert cells[OSV_60] == {"status": "CONFIRMED", "missing_quarters": [], "document_ids": [str(i) for i in documents]}
|
||||
assert cells[OSV_90] == {"status": "MISSING", "missing_quarters": [], "document_ids": []}
|
||||
assert cells[OSV_60] == {
|
||||
"document_type": "OSV",
|
||||
"account": "60",
|
||||
"status": "CONFIRMED",
|
||||
"missing_quarters": [],
|
||||
"document_ids": [str(i) for i in documents],
|
||||
}
|
||||
assert cells[OSV_90] == {
|
||||
"document_type": "OSV",
|
||||
"account": "90",
|
||||
"status": "MISSING",
|
||||
"missing_quarters": [],
|
||||
"document_ids": [],
|
||||
}
|
||||
assert {cell["status"] for cell in _cells(body, "ООО Б")} == {"MISSING"}
|
||||
|
||||
|
||||
@ -107,6 +119,8 @@ async def test_checklist_gap_needs_check_and_processing(auth_client: AuthClientF
|
||||
|
||||
first_cells = _cells(body, "ООО А")
|
||||
assert first_cells[OSV_60] == {
|
||||
"document_type": "OSV",
|
||||
"account": "60",
|
||||
"status": "MISSING",
|
||||
"missing_quarters": ["Q2"],
|
||||
"document_ids": [str(first_q1), str(third_q3)],
|
||||
@ -130,6 +144,25 @@ async def test_card_members_have_same_cells_as_checklist(auth_client: AuthClient
|
||||
assert card["members"][0]["id"] == str(first.id)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_card_cells_are_self_describing(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||
"""front-api-gaps D-2: ячейка несёт правило — фронт не сопоставляет ячейки с `rules` по индексу."""
|
||||
version, _ = await _bound_world(seeded)
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
checklist = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json()
|
||||
card = (await client.get(f"/consolidations/versions/{version.id}")).json()
|
||||
|
||||
expected = [(rule["document_type"], rule["account"]) for rule in checklist["rules"]]
|
||||
assert expected == EXPECTED_RULES
|
||||
for member in card["members"]:
|
||||
assert [(cell["document_type"], cell["account"]) for cell in member["checklist"]] == expected
|
||||
for row in checklist["rows"]:
|
||||
assert [(cell["document_type"], cell["account"]) for cell in row["cells"]] == expected
|
||||
balance = next(cell for cell in card["members"][0]["checklist"] if cell["document_type"] == "BALANCE")
|
||||
assert balance["account"] is None
|
||||
|
||||
|
||||
async def _bound_world(session: AsyncSession) -> tuple[ConsolidationVersion, list[LegalEntity]]:
|
||||
version, entities = await _world(session)
|
||||
for quarter in (Q1, Q2, Q3):
|
||||
|
||||
@ -20,8 +20,20 @@ def test_render() -> None:
|
||||
legal_entity_name='ООО "Альфа; Бета"',
|
||||
legal_entity_inn="7700000000",
|
||||
cells=[
|
||||
ChecklistCellOut(status=CellStatus.CONFIRMED, missing_quarters=[], document_ids=[]),
|
||||
ChecklistCellOut(status=CellStatus.MISSING, missing_quarters=[PeriodQuarter.Q2], document_ids=[]),
|
||||
ChecklistCellOut(
|
||||
document_type=DocumentType.BALANCE,
|
||||
account=None,
|
||||
status=CellStatus.CONFIRMED,
|
||||
missing_quarters=[],
|
||||
document_ids=[],
|
||||
),
|
||||
ChecklistCellOut(
|
||||
document_type=DocumentType.OSV,
|
||||
account="60",
|
||||
status=CellStatus.MISSING,
|
||||
missing_quarters=[PeriodQuarter.Q2],
|
||||
document_ids=[],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
@ -34,7 +46,9 @@ def test_render() -> None:
|
||||
|
||||
|
||||
def _cell(status: CellStatus, *quarters: PeriodQuarter) -> ChecklistCellOut:
|
||||
return ChecklistCellOut(status=status, missing_quarters=list(quarters), document_ids=[])
|
||||
return ChecklistCellOut(
|
||||
document_type=DocumentType.OSV, account="60", status=status, missing_quarters=list(quarters), document_ids=[]
|
||||
)
|
||||
|
||||
|
||||
def test_missing_quarters_listed_in_order() -> None:
|
||||
|
||||
@ -251,3 +251,9 @@ def test_consolidation_create_has_optional_selected_perimeter(schema: dict[str,
|
||||
create = schema["components"]["schemas"]["ConsolidationCreate"]
|
||||
assert "legal_entity_ids" in create["properties"]
|
||||
assert "legal_entity_ids" not in create.get("required", [])
|
||||
|
||||
|
||||
def test_checklist_cell_declares_its_rule(schema: dict[str, Any]) -> None:
|
||||
"""front-api-gaps D-2: ячейка чек-листа самоописывающая."""
|
||||
cell = schema["components"]["schemas"]["ChecklistCellOut"]
|
||||
assert {"document_type", "account", "status", "missing_quarters", "document_ids"} <= set(cell["required"])
|
||||
|
||||
@ -10,7 +10,7 @@ from pydantic import ValidationError
|
||||
|
||||
from src.domain.checklist import CellStatus, EntityFilesState
|
||||
from src.domain.consolidated_osv import CounterpartyType as DomainCounterpartyType
|
||||
from src.models.enums import CounterpartyMatchKind, FileStatus, PeriodQuarter
|
||||
from src.models.enums import CounterpartyMatchKind, DocumentType, FileStatus, PeriodQuarter
|
||||
from src.schemas import consolidations as schemas
|
||||
from src.schemas.consolidations import (
|
||||
MAX_MEMBERS,
|
||||
@ -123,10 +123,36 @@ def test_entity_files_state_is_frozen() -> None:
|
||||
def test_checklist_cell_status_is_the_domain_enum() -> None:
|
||||
"""Статус ячейки в схеме — `domain.checklist.CellStatus` (одно определение, T2.8): все значения принимаются."""
|
||||
for status in CellStatus:
|
||||
cell = ChecklistCellOut(status=status.value, missing_quarters=[], document_ids=[]) # type: ignore[arg-type]
|
||||
cell = ChecklistCellOut(
|
||||
document_type=DocumentType.OSV,
|
||||
account="60",
|
||||
status=status.value, # type: ignore[arg-type]
|
||||
missing_quarters=[],
|
||||
document_ids=[],
|
||||
)
|
||||
assert cell.status is status
|
||||
with pytest.raises(ValidationError):
|
||||
ChecklistCellOut(status="UNKNOWN", missing_quarters=[], document_ids=[]) # type: ignore[arg-type]
|
||||
ChecklistCellOut(
|
||||
document_type=DocumentType.OSV,
|
||||
account="60",
|
||||
status="UNKNOWN", # type: ignore[arg-type]
|
||||
missing_quarters=[],
|
||||
document_ids=[],
|
||||
)
|
||||
|
||||
|
||||
def test_checklist_cell_requires_its_rule() -> None:
|
||||
"""D-2: `document_type` обязателен, `account` — обязателен, но может быть `None` (ББ/ОФР)."""
|
||||
with pytest.raises(ValidationError):
|
||||
ChecklistCellOut(status=CellStatus.MISSING, missing_quarters=[], document_ids=[]) # type: ignore[call-arg]
|
||||
cell = ChecklistCellOut(
|
||||
document_type=DocumentType.BALANCE,
|
||||
account=None,
|
||||
status=CellStatus.MISSING,
|
||||
missing_quarters=[],
|
||||
document_ids=[],
|
||||
)
|
||||
assert cell.account is None
|
||||
|
||||
|
||||
def test_counterparty_type_is_the_domain_enum() -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user