feat(consolidations): тип ЮЛ и счета ОСВ к периоду версии у участника периметра (#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
678cc4566f
commit
d782637bc1
@ -14,6 +14,7 @@ from sqlalchemy import Select, case, exists, false, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.domain.checklist import EntityFilesState
|
||||
from src.domain.osv_mapping import account_sort_key
|
||||
from src.domain.policy import VisibilityScope, company_groups_visible
|
||||
from src.models.access import AppUser
|
||||
from src.models.accounting import ChartOfAccount
|
||||
@ -350,6 +351,36 @@ async def unconfirmed_documents(session: AsyncSession, entity_ids: Collection[uu
|
||||
return list((await session.execute(stmt)).scalars())
|
||||
|
||||
|
||||
async def osv_accounts(
|
||||
session: AsyncSession, entity_ids: Collection[uuid.UUID], year: int, quarters: Collection[PeriodQuarter]
|
||||
) -> dict[uuid.UUID, list[str]]:
|
||||
"""Счета ОСВ, загруженные к периоду версии (колонка «ОСВ» панели [34]; front-api-gaps D-3): уникальные
|
||||
`osv_account_code` документов `OSV` ЮЛ за год `year` и кварталы `quarters`; документ не `ERROR`, файл не
|
||||
`SUPERSEDED`/`ERROR`, квартал и код счёта заданы; статус подтверждения и привязка к версии не важны. Порядок —
|
||||
натуральный (`account_sort_key`); ЮЛ без документов в словаре нет."""
|
||||
if not entity_ids:
|
||||
return {}
|
||||
stmt = (
|
||||
select(Document.legal_entity_id, Document.osv_account_code)
|
||||
.join(StoredFile, StoredFile.id == Document.stored_file_id)
|
||||
.where(
|
||||
Document.legal_entity_id.in_(list(entity_ids)),
|
||||
Document.document_type == DocumentType.OSV,
|
||||
Document.period_year == year,
|
||||
Document.period_quarter.in_(list(quarters)),
|
||||
Document.status != DocumentStatus.ERROR,
|
||||
StoredFile.status.not_in([FileStatus.SUPERSEDED, FileStatus.ERROR]),
|
||||
Document.osv_account_code.is_not(None),
|
||||
Document.osv_account_code != "",
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
accounts: dict[uuid.UUID, list[str]] = defaultdict(list)
|
||||
for entity_id, code in await session.execute(stmt):
|
||||
accounts[entity_id].append(code)
|
||||
return {entity_id: sorted(codes, key=account_sort_key) for entity_id, codes in accounts.items()}
|
||||
|
||||
|
||||
def _summary_entries(version_id: uuid.UUID) -> Select[tuple[OsvEntry]]:
|
||||
"""Строки свода версии: контрагентские (`COUNTERPARTY`) строки BU/SUM ОСВ-документов, привязанных к версии."""
|
||||
return (
|
||||
|
||||
@ -20,6 +20,7 @@ from src.models.enums import (
|
||||
ConsolidationStatus,
|
||||
CounterpartyMatchKind,
|
||||
DocumentType,
|
||||
EntityType,
|
||||
PeriodQuarter,
|
||||
)
|
||||
|
||||
@ -110,13 +111,16 @@ class ChecklistCellOut(BaseModel):
|
||||
|
||||
|
||||
class MemberOut(BaseModel):
|
||||
"""ЮЛ периметра версии; `checklist` — ячейки в порядке правил подхода (как `rules[]` чек-листа)."""
|
||||
"""ЮЛ периметра версии (front-api-gaps D-2, D-3): тип ЮЛ, самоописывающие ячейки чек-листа в порядке правил подхода
|
||||
и счета ОСВ, загруженные к периоду версии (`osv_accounts` — «что загружено», готовность — в ячейках)."""
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
inn: str
|
||||
entity_type: EntityType
|
||||
is_head_entity: bool
|
||||
checklist: list[ChecklistCellOut]
|
||||
osv_accounts: list[str]
|
||||
|
||||
|
||||
class HistoryOut(BaseModel):
|
||||
|
||||
@ -19,7 +19,7 @@ from src.api.dependencies.audit import RequestAudit
|
||||
from src.common.clock import utc_now
|
||||
from src.common.exceptions import ConflictError, DomainValidationError, NotFoundError
|
||||
from src.domain import checklist as checklist_domain
|
||||
from src.domain.consolidation_periods import reporting_date
|
||||
from src.domain.consolidation_periods import quarters_through, reporting_date
|
||||
from src.domain.policy import FINISHED_STATUSES, VisibilityScope
|
||||
from src.models.access import AppUser
|
||||
from src.models.consolidation import (
|
||||
@ -379,7 +379,8 @@ class ConsolidationService:
|
||||
async def _card(self, version: ConsolidationVersion, consolidation: Consolidation) -> VersionCard:
|
||||
"""Карточка версии: версия, консолидация, ГК, инициатор, периметр, история, активная блокировка.
|
||||
|
||||
Чек-лист у членов — те же ячейки, что у `GET …/checklist`, в порядке правил подхода.
|
||||
Чек-лист у членов — те же ячейки, что у `GET …/checklist`, в порядке правил подхода; у членов — тип ЮЛ и счета
|
||||
ОСВ к периоду версии (front-api-gaps D-3).
|
||||
"""
|
||||
group = await directories_repo.get_company_group(self._session, self._scope, consolidation.company_group_id)
|
||||
initiator = await self._session.get(AppUser, version.initiator_id)
|
||||
@ -391,6 +392,12 @@ class ConsolidationService:
|
||||
history = await repo.history(self._session, version.id)
|
||||
brief = self._brief(version, locks.get(version.id), len(members))
|
||||
rules, cells = await self._checklist_cells(version, consolidation, [entity for _, entity in members])
|
||||
accounts = await repo.osv_accounts(
|
||||
self._session,
|
||||
[entity.id for _, entity in members],
|
||||
consolidation.year,
|
||||
quarters_through(consolidation.quarter),
|
||||
)
|
||||
return VersionCard(
|
||||
**brief.model_dump(),
|
||||
consolidation_id=consolidation.id,
|
||||
@ -408,10 +415,12 @@ class ConsolidationService:
|
||||
id=entity.id,
|
||||
name=entity.name,
|
||||
inn=entity.inn,
|
||||
entity_type=entity.entity_type,
|
||||
is_head_entity=member.is_head_entity,
|
||||
checklist=[
|
||||
self._cell_out(rule, cell) for rule, cell in zip(rules, cells[position][1], strict=True)
|
||||
],
|
||||
osv_accounts=accounts.get(entity.id, []),
|
||||
)
|
||||
for position, (member, entity) in enumerate(members)
|
||||
],
|
||||
|
||||
@ -20,15 +20,27 @@ from src.models.consolidation import (
|
||||
ConsolidationVersion,
|
||||
)
|
||||
from src.models.directories import CompanyGroup, LegalEntity, RegionalOffice, RelatedGroup, RelatedGroupMember
|
||||
from src.models.enums import AuditAction, AuditSection, ConsolidationStatus, PeriodQuarter, RelatedGroupKind
|
||||
from src.models.enums import (
|
||||
AuditAction,
|
||||
AuditSection,
|
||||
ConsolidationStatus,
|
||||
DocumentStatus,
|
||||
EntityType,
|
||||
FileStatus,
|
||||
PeriodQuarter,
|
||||
RecognitionStatus,
|
||||
RelatedGroupKind,
|
||||
)
|
||||
from tests.conftest import AuthClientFactory
|
||||
from tests.factories import (
|
||||
make_company_group,
|
||||
make_consolidation,
|
||||
make_document,
|
||||
make_legal_entity,
|
||||
make_lock,
|
||||
make_membership,
|
||||
make_related_group,
|
||||
make_stored_file,
|
||||
make_version,
|
||||
)
|
||||
|
||||
@ -560,3 +572,87 @@ async def test_version_card_requires_consolidation_view(auth_client: AuthClientF
|
||||
client = await auth_client("admin@armka.local") # у роли ADMIN права CONSOLIDATION_VIEW нет
|
||||
response = await client.get(f"{VERSIONS}/{versions['q3v1'].id}")
|
||||
assert response.status_code == HTTP_FORBIDDEN
|
||||
|
||||
|
||||
async def _osv( # noqa: PLR0913 - параметры документа сценария
|
||||
session: AsyncSession,
|
||||
entity: LegalEntity,
|
||||
account: str,
|
||||
year: int,
|
||||
quarter: PeriodQuarter,
|
||||
*,
|
||||
status: DocumentStatus = DocumentStatus.RECOGNIZED,
|
||||
file_status: FileStatus = FileStatus.PROCESSED,
|
||||
) -> None:
|
||||
"""ОСВ-документ ЮЛ в отдельном файле (на файл — одна ОСВ по счёту)."""
|
||||
file, job = await make_stored_file(session, entity=entity, status=file_status, job_status=RecognitionStatus.done)
|
||||
await make_document(session, file=file, job=job, account=account, year=year, quarter=quarter, status=status)
|
||||
|
||||
|
||||
async def _card_world(
|
||||
session: AsyncSession, quarter: PeriodQuarter = PeriodQuarter.Q3
|
||||
) -> tuple[ConsolidationVersion, LegalEntity, LegalEntity]:
|
||||
"""Версия 2024 `quarter` ГК Ростовского филиала: ЮЛ «ООО А» и ИП «ИП Б»."""
|
||||
group = await make_company_group(session, name="ГК Карточка", office=await _office(session, ROSTOV))
|
||||
company = await make_legal_entity(session, name="ООО А", group=group)
|
||||
person = await make_legal_entity(session, name="ИП Б", group=group, entity_type=EntityType.ИП, inn="770100000012")
|
||||
_, version = await make_consolidation(
|
||||
session,
|
||||
group=group,
|
||||
year=2024,
|
||||
quarter=quarter,
|
||||
members=[company, person],
|
||||
initiator=await _user(session, KA_RF),
|
||||
)
|
||||
return version, company, person
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_card_member_entity_type(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||
version, company, person = await _card_world(seeded)
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
card = (await client.get(f"{VERSIONS}/{version.id}")).json()
|
||||
|
||||
types = {member["id"]: member["entity_type"] for member in card["members"]}
|
||||
assert types == {str(company.id): "ЮЛ", str(person.id): "ИП"}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_card_member_osv_accounts_loaded_to_version_period(
|
||||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||||
) -> None:
|
||||
"""D-3 (вариант B): счета действующих ОСВ за год версии с кварталом ≤ версии, в любом статусе, кроме ERROR."""
|
||||
version, company, person = await _card_world(seeded)
|
||||
q1, q2, q3, q4 = PeriodQuarter.Q1, PeriodQuarter.Q2, PeriodQuarter.Q3, PeriodQuarter.Q4
|
||||
await _osv(seeded, company, "60", 2024, q1) # RECOGNIZED
|
||||
await _osv(seeded, company, "60", 2024, q2) # тот же счёт ещё раз — один раз в списке
|
||||
await _osv(seeded, company, "51", 2024, q3, status=DocumentStatus.CONFIRMED)
|
||||
await _osv(seeded, company, "58.03", 2024, q3, status=DocumentStatus.NEEDS_CHECK)
|
||||
await _osv(seeded, company, "62", 2024, q4) # позже квартала версии
|
||||
await _osv(seeded, company, "01", 2023, q3) # прошлый год
|
||||
await _osv(seeded, company, "76", 2024, q3, file_status=FileStatus.SUPERSEDED) # заменённый файл
|
||||
await _osv(seeded, company, "90", 2024, q3, status=DocumentStatus.ERROR) # документ в ошибке
|
||||
await _osv(seeded, company, "10", 2024, q3, file_status=FileStatus.ERROR) # файл в ошибке
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
card = (await client.get(f"{VERSIONS}/{version.id}")).json()
|
||||
|
||||
accounts = {member["id"]: member["osv_accounts"] for member in card["members"]}
|
||||
assert accounts == {str(company.id): ["51", "58.03", "60"], str(person.id): []}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_card_member_osv_accounts_q1_version_ignores_later_quarters(
|
||||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||||
) -> None:
|
||||
"""Review Focus: версия за Q1 — документы Q2 того же года не входят."""
|
||||
version, company, _ = await _card_world(seeded, PeriodQuarter.Q1)
|
||||
await _osv(seeded, company, "60", 2024, PeriodQuarter.Q1)
|
||||
await _osv(seeded, company, "62", 2024, PeriodQuarter.Q2)
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
card = (await client.get(f"{VERSIONS}/{version.id}")).json()
|
||||
|
||||
member = next(member for member in card["members"] if member["id"] == str(company.id))
|
||||
assert member["osv_accounts"] == ["60"]
|
||||
|
||||
@ -257,3 +257,9 @@ 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"])
|
||||
|
||||
|
||||
def test_version_member_declares_type_and_osv_accounts(schema: dict[str, Any]) -> None:
|
||||
"""front-api-gaps D-3: участник периметра — тип ЮЛ и счета ОСВ к периоду версии."""
|
||||
member = schema["components"]["schemas"]["MemberOut"]
|
||||
assert {"entity_type", "osv_accounts", "checklist"} <= set(member["required"])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user