"""Фаза 2: применение проверенного набора к БД — upsert по натуральному ключу, без удалений и без commit.""" import uuid from typing import Any from sqlalchemy import func, select from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession from scripts.seed_lib.apply_rules import apply_elimination_rules, apply_required_document_rules from scripts.seed_lib.model import SeedData, SeedReport, TableStat from src.models.access import Permission, Role, RolePermission from src.models.accounting import ChartOfAccount from src.models.directories import RegionalOffice async def _upsert( session: AsyncSession, model: Any, key: str, rows: list[dict[str, Any]], stat: TableStat, ) -> None: """Вставить новые и обновить изменившиеся строки по натуральному ключу `key`; удалений нет.""" if not rows: return key_column = getattr(model, key) value_names = [name for name in rows[0] if name != key] existing: dict[Any, tuple[Any, ...]] = { row[0]: tuple(row[1:]) for row in (await session.execute(select(key_column, *(getattr(model, name) for name in value_names)))).all() } changed = [] for row in rows: current = existing.get(row[key]) if current is None: stat.inserted += 1 changed.append(row) elif current != tuple(row[name] for name in value_names): stat.updated += 1 changed.append(row) else: stat.unchanged += 1 if changed: statement = pg_insert(model) statement = statement.on_conflict_do_update( index_elements=[key_column], set_={name: statement.excluded[name] for name in value_names} ) await session.execute(statement, changed) async def _apply_role_permissions(session: AsyncSession, data: SeedData, stat: TableStat) -> None: role_ids = {code: id_ for id_, code in (await session.execute(select(Role.id, Role.code))).all()} permission_ids = {code: id_ for id_, code in (await session.execute(select(Permission.id, Permission.code))).all()} existing = set((await session.execute(select(RolePermission.role_id, RolePermission.permission_id))).all()) new_rows = [] for role_code, codes in data.role_permissions.items(): for code in codes: pair = (role_ids[role_code], permission_ids[code]) if pair in existing: stat.unchanged += 1 else: stat.inserted += 1 new_rows.append( {"role_id": pair[0], "permission_id": pair[1], "granted_by": None, "granted_at": func.now()} ) if new_rows: # .values(): func.now() допустим только во встроенном VALUES, не в executemany statement = ( pg_insert(RolePermission) .values(new_rows) .on_conflict_do_nothing(index_elements=["role_id", "permission_id"]) ) await session.execute(statement) async def _apply_chart_of_accounts(session: AsyncSession, data: SeedData, stat: TableStat) -> None: """План счетов: upsert по `code` в порядке глубины (родитель раньше потомков), одним `run_seed` и одной транзакцией. Идентификаторы новых строк генерирует приложение заранее, поэтому `parent_id` известен при вставке и второй проход не нужен; существующим строкам при расхождении с файлом обновляются поля и `parent_id`. Удалений нет. """ if not data.accounts: return stored = ( await session.execute( select( ChartOfAccount.code, ChartOfAccount.id, ChartOfAccount.parent_id, ChartOfAccount.name, ChartOfAccount.kind, ChartOfAccount.row_type, ChartOfAccount.is_off_balance, ) ) ).all() ids = {row.code: row.id for row in stored} existing = {row.code: (row.parent_id, row.name, row.kind, row.row_type, row.is_off_balance) for row in stored} for code in data.accounts: ids.setdefault(code, uuid.uuid4()) rows = [] for code, account in sorted(data.accounts.items(), key=lambda item: item[0].count(".")): parent_id = ids[account.parent_code] if account.parent_code is not None else None values = (parent_id, account.name, account.kind, account.row_type, account.is_off_balance) if code not in existing: stat.inserted += 1 elif existing[code] != values: stat.updated += 1 else: stat.unchanged += 1 continue rows.append( { "id": ids[code], "code": code, "parent_id": parent_id, "name": account.name, "kind": account.kind, "row_type": account.row_type, "is_off_balance": account.is_off_balance, } ) if rows: statement = pg_insert(ChartOfAccount) statement = statement.on_conflict_do_update( index_elements=[ChartOfAccount.code], set_={ name: statement.excluded[name] for name in ("parent_id", "name", "kind", "row_type", "is_off_balance") }, ) await session.execute(statement, rows) async def apply_seed(session: AsyncSession, data: SeedData) -> SeedReport: """Фаза 2: upsert набора в порядке зависимостей. Без commit — транзакцией управляет вызывающий.""" report = SeedReport() await _upsert( session, RegionalOffice, "name", [{"name": o.name, "is_head_office": o.is_head_office, "is_active": o.is_active} for o in data.regional_offices], report.stat("regional_office"), ) await _upsert( session, Role, "code", [ { "code": r.code, "name": r.name, "description": r.description, "is_system": r.is_system, "is_active": r.is_active, } for r in data.roles ], report.stat("role"), ) await _upsert( session, Permission, "code", [ { "code": p.code, "object_type": p.object_type, "action": p.action, "name": p.name, "description": p.description, } for p in data.permissions ], report.stat("permission"), ) await _apply_role_permissions(session, data, report.stat("role_permission")) await _apply_chart_of_accounts(session, data, report.stat("chart_of_account")) await apply_elimination_rules( session, data, report.stat("elimination_rule"), report.stat("elimination_rule_account") ) await apply_required_document_rules(session, data, report.stat("required_document_rule")) return report