Andrey Epifancev 900fdc8ece fix: seed не возвращает снятые права — матрица только в пустую role_permission (#9 п.7)
`_apply_role_permissions` (`scripts/seed_lib/apply.py`) загружает матрицу из
`role_permissions.yaml` только при пустой `role_permission`; иначе шаг
пропускается целиком с причиной в отчёте (`TableStat.skipped_reason`,
«role_permission: пропущено — …»). Права, снятые администратором, не
возвращаются; `role` и `permission` по-прежнему upsert'ятся.

Тесты: повторный прогон — «пропущено», матрица та же; снятая связь после
повторного seed не возвращается. README «Администрирование» и раздел seed
переписаны под новое поведение.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:30:09 +04:00

158 lines
5.2 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.

"""Модель проверенного набора seed и отчёта о применении (без доступа к БД)."""
import datetime
from dataclasses import dataclass, field
from src.models.enums import (
AccountKind,
AccountRowType,
BusinessSegment,
ConsolidationApproach,
DocumentType,
PermissionAction,
PermissionObject,
StatementType,
)
class SeedError(Exception):
"""Ошибка файлов seed (отсутствие, формат, ссылочная целостность); сообщение — по-русски, с файлом и записью."""
@dataclass(frozen=True)
class RegionalOfficeSeed:
name: str
is_head_office: bool
is_active: bool
@dataclass(frozen=True)
class RoleSeed:
code: str
name: str
description: str | None
is_system: bool
is_active: bool
@dataclass(frozen=True)
class PermissionSeed:
code: str
object_type: PermissionObject
action: PermissionAction
name: str
description: str | None
@dataclass(frozen=True)
class AccountSeed:
"""Строка плана счетов; `code` нормализован (60.01, 76.07.1), `parent_code` — код родителя или None."""
code: str
parent_code: str | None
name: str
kind: AccountKind | None
row_type: AccountRowType
is_off_balance: bool
@dataclass(frozen=True)
class RuleSideSeed:
"""Состав счетов одной стороны правила: `accounts` — включённые, `excluded` — исключения внутри маски."""
accounts: tuple[str, ...]
excluded: tuple[str, ...]
@dataclass(frozen=True)
class EliminationRuleSeed:
"""Правило элиминации версии `version_no` (§4); `sides` — company_no (1, 2) -> состав счетов."""
statement_type: StatementType
rule_no: int
version_no: int
article_1_line: str
article_2_line: str
side_1: str
measure: str
elimination_mode: str
vat_handling: str | None
excise_adjust: bool
comment: str | None
valid_from: datetime.date
sides: dict[int, RuleSideSeed]
@dataclass(frozen=True)
class RequiredDocumentRuleSeed:
"""Строка чек-листа; `account_code` задан только для OSV."""
approach: ConsolidationApproach
document_type: DocumentType
account_code: str | None
is_mandatory: bool
@dataclass(frozen=True)
class UserSeed:
"""Dev-пользователь; `regional_office` — имя филиала, `role_codes` — коды ролей (в dev-наборе ровно одна)."""
email: str
full_name: str
employee_no: str | None
regional_office: str
business_segment: BusinessSegment | None
is_active: bool
role_codes: tuple[str, ...]
@dataclass
class SeedData:
"""Проверенный набор seed; ничего не знает о БД."""
regional_offices: list[RegionalOfficeSeed] = field(default_factory=list)
roles: list[RoleSeed] = field(default_factory=list)
permissions: list[PermissionSeed] = field(default_factory=list)
#: код роли -> коды прав (порядок как в файле)
role_permissions: dict[str, list[str]] = field(default_factory=dict)
#: код счёта -> счёт; порядок файла (родитель раньше потомков). Реестр кодов для загрузчика правил: `account_codes`
accounts: dict[str, AccountSeed] = field(default_factory=dict)
elimination_rules: list[EliminationRuleSeed] = field(default_factory=list)
required_document_rules: list[RequiredDocumentRuleSeed] = field(default_factory=list)
#: только при `--dev` (seed/dev/users.yaml); при `--dev` dev-филиалы дописываются в `regional_offices`
users: list[UserSeed] = field(default_factory=list)
def account_codes(data: SeedData) -> frozenset[str]:
"""Коды плана счетов набора: правила элиминации и чек-лист проверяют свои ссылки на счета по этому реестру."""
return frozenset(data.accounts)
@dataclass
class TableStat:
inserted: int = 0
updated: int = 0
unchanged: int = 0
#: шаг не выполнялся (например, матрица прав уже заполнена); текст — для отчёта CLI
skipped_reason: str | None = None
@dataclass
class SeedReport:
"""Итог применения: по таблицам — сколько строк вставлено / обновлено / не изменилось (или почему шаг пропущен)."""
tables: dict[str, TableStat] = field(default_factory=dict)
def stat(self, table: str) -> TableStat:
return self.tables.setdefault(table, TableStat())
def format(self) -> str:
lines = ["Seed применён (вставлено / обновлено / без изменений):"]
lines += [
f" {name}: пропущено — {stat.skipped_reason}"
if stat.skipped_reason is not None
else f" {name}: {stat.inserted} / {stat.updated} / {stat.unchanged}"
for name, stat in self.tables.items()
]
return "\n".join(lines)