arm-core/api/scripts/seed_lib/apply_rules.py

144 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Фаза 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()