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):
|
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)
|
status: CellStatus # единственное определение — `domain.checklist.CellStatus` (T2.8)
|
||||||
missing_quarters: list[PeriodQuarter]
|
missing_quarters: list[PeriodQuarter]
|
||||||
document_ids: list[uuid.UUID]
|
document_ids: list[uuid.UUID]
|
||||||
|
|||||||
@ -192,7 +192,7 @@ class ConsolidationService:
|
|||||||
legal_entity_id=entity.id,
|
legal_entity_id=entity.id,
|
||||||
legal_entity_name=entity.name,
|
legal_entity_name=entity.name,
|
||||||
legal_entity_inn=entity.inn,
|
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
|
for entity, cells in rows
|
||||||
],
|
],
|
||||||
@ -236,9 +236,14 @@ class ConsolidationService:
|
|||||||
return rules, rows
|
return rules, rows
|
||||||
|
|
||||||
@staticmethod
|
@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(
|
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:
|
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])
|
locks = await repo.active_locks(self._session, [version.id])
|
||||||
history = await repo.history(self._session, version.id)
|
history = await repo.history(self._session, version.id)
|
||||||
brief = self._brief(version, locks.get(version.id), len(members))
|
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(
|
return VersionCard(
|
||||||
**brief.model_dump(),
|
**brief.model_dump(),
|
||||||
consolidation_id=consolidation.id,
|
consolidation_id=consolidation.id,
|
||||||
@ -404,7 +409,9 @@ class ConsolidationService:
|
|||||||
name=entity.name,
|
name=entity.name,
|
||||||
inn=entity.inn,
|
inn=entity.inn,
|
||||||
is_head_entity=member.is_head_entity,
|
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)
|
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
|
assert body["rows"][1]["legal_entity_inn"] == second.inn
|
||||||
cells = _cells(body, "ООО А")
|
cells = _cells(body, "ООО А")
|
||||||
assert len(cells) == len(EXPECTED_RULES)
|
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_60] == {
|
||||||
assert cells[OSV_90] == {"status": "MISSING", "missing_quarters": [], "document_ids": []}
|
"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"}
|
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, "ООО А")
|
first_cells = _cells(body, "ООО А")
|
||||||
assert first_cells[OSV_60] == {
|
assert first_cells[OSV_60] == {
|
||||||
|
"document_type": "OSV",
|
||||||
|
"account": "60",
|
||||||
"status": "MISSING",
|
"status": "MISSING",
|
||||||
"missing_quarters": ["Q2"],
|
"missing_quarters": ["Q2"],
|
||||||
"document_ids": [str(first_q1), str(third_q3)],
|
"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)
|
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]]:
|
async def _bound_world(session: AsyncSession) -> tuple[ConsolidationVersion, list[LegalEntity]]:
|
||||||
version, entities = await _world(session)
|
version, entities = await _world(session)
|
||||||
for quarter in (Q1, Q2, Q3):
|
for quarter in (Q1, Q2, Q3):
|
||||||
|
|||||||
@ -20,8 +20,20 @@ def test_render() -> None:
|
|||||||
legal_entity_name='ООО "Альфа; Бета"',
|
legal_entity_name='ООО "Альфа; Бета"',
|
||||||
legal_entity_inn="7700000000",
|
legal_entity_inn="7700000000",
|
||||||
cells=[
|
cells=[
|
||||||
ChecklistCellOut(status=CellStatus.CONFIRMED, missing_quarters=[], document_ids=[]),
|
ChecklistCellOut(
|
||||||
ChecklistCellOut(status=CellStatus.MISSING, missing_quarters=[PeriodQuarter.Q2], document_ids=[]),
|
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:
|
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:
|
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"]
|
create = schema["components"]["schemas"]["ConsolidationCreate"]
|
||||||
assert "legal_entity_ids" in create["properties"]
|
assert "legal_entity_ids" in create["properties"]
|
||||||
assert "legal_entity_ids" not in create.get("required", [])
|
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.checklist import CellStatus, EntityFilesState
|
||||||
from src.domain.consolidated_osv import CounterpartyType as DomainCounterpartyType
|
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 import consolidations as schemas
|
||||||
from src.schemas.consolidations import (
|
from src.schemas.consolidations import (
|
||||||
MAX_MEMBERS,
|
MAX_MEMBERS,
|
||||||
@ -123,10 +123,36 @@ def test_entity_files_state_is_frozen() -> None:
|
|||||||
def test_checklist_cell_status_is_the_domain_enum() -> None:
|
def test_checklist_cell_status_is_the_domain_enum() -> None:
|
||||||
"""Статус ячейки в схеме — `domain.checklist.CellStatus` (одно определение, T2.8): все значения принимаются."""
|
"""Статус ячейки в схеме — `domain.checklist.CellStatus` (одно определение, T2.8): все значения принимаются."""
|
||||||
for status in CellStatus:
|
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
|
assert cell.status is status
|
||||||
with pytest.raises(ValidationError):
|
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:
|
def test_counterparty_type_is_the_domain_enum() -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user