144 lines
6.4 KiB
Python
144 lines
6.4 KiB
Python
"""Фаза 2: правила элиминации и чек-лист обязательных документов — upsert без удалений и без commit."""
|
||
|
||
import uuid
|
||
from typing import Any
|
||
|
||
from sqlalchemy import select, update
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from scripts.seed_lib.model import SeedData, TableStat
|
||
from src.models.accounting import ChartOfAccount, EliminationRule, EliminationRuleAccount
|
||
from src.models.audit_ex import RequiredDocumentRule
|
||
|
||
_RULE_VALUE_FIELDS = (
|
||
"article_1_line",
|
||
"article_2_line",
|
||
"side_1",
|
||
"measure",
|
||
"elimination_mode",
|
||
"vat_handling",
|
||
"excise_adjust",
|
||
"comment",
|
||
"valid_from",
|
||
)
|
||
|
||
|
||
async def _account_ids(session: AsyncSession) -> dict[str, uuid.UUID]:
|
||
return {code: id_ for id_, code in (await session.execute(select(ChartOfAccount.id, ChartOfAccount.code))).all()}
|
||
|
||
|
||
async def apply_elimination_rules(
|
||
session: AsyncSession, data: SeedData, rule_stat: TableStat, account_stat: TableStat
|
||
) -> None:
|
||
"""Правила — upsert по (statement_type, rule_no, version_no); затем состав счетов приводится к файлу."""
|
||
if not data.elimination_rules:
|
||
return
|
||
rule_ids = await _upsert_rules(session, data, rule_stat)
|
||
await _upsert_rule_accounts(session, data, rule_ids, account_stat)
|
||
|
||
|
||
async def _upsert_rules(
|
||
session: AsyncSession, data: SeedData, stat: TableStat
|
||
) -> dict[tuple[Any, int, int], uuid.UUID]:
|
||
stored = {
|
||
(rule.statement_type, rule.rule_no, rule.version_no): rule
|
||
for rule in (await session.scalars(select(EliminationRule))).all()
|
||
}
|
||
rule_ids: dict[tuple[Any, int, int], uuid.UUID] = {}
|
||
for seed in data.elimination_rules:
|
||
key = (seed.statement_type, seed.rule_no, seed.version_no)
|
||
values = {name: getattr(seed, name) for name in _RULE_VALUE_FIELDS}
|
||
current = stored.get(key)
|
||
if current is None:
|
||
rule_id = rule_ids[key] = uuid.uuid4()
|
||
session.add(
|
||
EliminationRule(
|
||
id=rule_id,
|
||
statement_type=seed.statement_type,
|
||
rule_no=seed.rule_no,
|
||
version_no=seed.version_no,
|
||
**values,
|
||
)
|
||
)
|
||
stat.inserted += 1
|
||
continue
|
||
rule_ids[key] = current.id
|
||
if any(getattr(current, name) != value for name, value in values.items()):
|
||
for name, value in values.items():
|
||
setattr(current, name, value)
|
||
stat.updated += 1
|
||
else:
|
||
stat.unchanged += 1
|
||
await session.flush()
|
||
return rule_ids
|
||
|
||
|
||
async def _upsert_rule_accounts(
|
||
session: AsyncSession, data: SeedData, rule_ids: dict[tuple[Any, int, int], uuid.UUID], stat: TableStat
|
||
) -> None:
|
||
"""Ключ внутри шага — (rule_id, company_no, account_id): отсутствующие вставляются, у существующих при расхождении
|
||
обновляется `is_excluded`. Уникального индекса в БД нет (DBML), поэтому выбор и сравнение — в Python.
|
||
Удалений нет: счета, убранные из файла, остаются в БД.
|
||
"""
|
||
account_ids = await _account_ids(session)
|
||
existing: dict[tuple[uuid.UUID, int, uuid.UUID], tuple[uuid.UUID, bool]] = {}
|
||
for row in (await session.execute(select(EliminationRuleAccount))).scalars().all():
|
||
existing.setdefault((row.rule_id, row.company_no, row.account_id), (row.id, row.is_excluded))
|
||
for seed in data.elimination_rules:
|
||
rule_id = rule_ids[(seed.statement_type, seed.rule_no, seed.version_no)]
|
||
for company_no, side in seed.sides.items():
|
||
wanted = [(code, False) for code in side.accounts] + [(code, True) for code in side.excluded]
|
||
for code, is_excluded in wanted:
|
||
account_id = account_ids[code]
|
||
current = existing.get((rule_id, company_no, account_id))
|
||
if current is None:
|
||
session.add(
|
||
EliminationRuleAccount(
|
||
rule_id=rule_id, company_no=company_no, account_id=account_id, is_excluded=is_excluded
|
||
)
|
||
)
|
||
stat.inserted += 1
|
||
elif current[1] != is_excluded:
|
||
await session.execute(
|
||
update(EliminationRuleAccount)
|
||
.where(EliminationRuleAccount.id == current[0])
|
||
.values(is_excluded=is_excluded)
|
||
)
|
||
stat.updated += 1
|
||
else:
|
||
stat.unchanged += 1
|
||
await session.flush()
|
||
|
||
|
||
async def apply_required_document_rules(session: AsyncSession, data: SeedData, stat: TableStat) -> None:
|
||
"""Чек-лист — upsert по (approach, document_type, account_id) с учётом NULL в `account_id` (BALANCE / INCOME).
|
||
|
||
Уникальный индекс трактует NULL как различные значения и допускает дубли, поэтому `ON CONFLICT` не годится:
|
||
существующие строки читаются, ключ сравнивается в Python (None == None). Удалений нет.
|
||
"""
|
||
if not data.required_document_rules:
|
||
return
|
||
account_ids = await _account_ids(session)
|
||
existing: dict[tuple[Any, Any, uuid.UUID | None], RequiredDocumentRule] = {}
|
||
for row in (await session.scalars(select(RequiredDocumentRule))).all():
|
||
existing.setdefault((row.approach, row.document_type, row.account_id), row)
|
||
for seed in data.required_document_rules:
|
||
account_id = account_ids[seed.account_code] if seed.account_code is not None else None
|
||
current = existing.get((seed.approach, seed.document_type, account_id))
|
||
if current is None:
|
||
session.add(
|
||
RequiredDocumentRule(
|
||
approach=seed.approach,
|
||
document_type=seed.document_type,
|
||
account_id=account_id,
|
||
is_mandatory=seed.is_mandatory,
|
||
)
|
||
)
|
||
stat.inserted += 1
|
||
elif current.is_mandatory != seed.is_mandatory:
|
||
current.is_mandatory = seed.is_mandatory
|
||
stat.updated += 1
|
||
else:
|
||
stat.unchanged += 1
|
||
await session.flush()
|