arm-core/api/scripts/seed.py
Andrey Epifancev 93cd1f8aa0 feat(seed): dev-ГК под документы fixtures — «Русский Бисквит» из выгрузки ЕЦП и владельцы форм ДКБ
seed/dev/company_groups.yaml: ГК «Русский Бисквит» ровно как в fixtures/directories/ecp-export-russkiy-biskvit.xlsx
(20 ЮЛ/ИП, crm_id, филиал «Санкт-Петербургский РФ», ГСК из РБ и Счастливочной), ГК Унагранде/НПЗ/Межениновская по
ИНН из форм ББ/ОФР; алиасы под имена контрагентов и шапок ОСВ («Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ», ...), чтобы
документы из fixtures ложились на «своих» ЮЛ и резолвились. Загрузчик проверяет ИНН, виды, ссылки членов и
уникальность «ЮЛ в одной ГСК/ГСЗ» до записи; apply — upsert по name/inn/(ГК, name), коды из последовательностей
API, без удалений. `--no-dev-groups` / `run_seed(dev_groups=False)` — dev-данные без ГК; фикстуры тестов API
используют его, чтобы начинать с пустых справочников.

make check: 1132 passed; интеграция seed/import: 23 passed. Два теста падают и на main из-за обновлённых golden
(test_mapping_golden 88 → 113, test_consolidation_fixtures UNMAPPED_TOTAL) — отдельная issue.

Closes #31

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 17:05:46 +04:00

142 lines
8.8 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.

"""Загрузчик справочников продукта: `python -m scripts.seed [--dev]` (запуск из `api/`).
Spec reference-data-seed; design D-4 и решение #18 (R-5): `run_seed(session, *, dev)` не делает commit — коммитит CLI.
Источник данных — `api/seed/core/*.yaml|csv` (+ `api/seed/dev/` при `--dev`).
Загрузка двухфазная:
1. `load_seed` (`seed_lib/loaders.py`) читает ВСЕ файлы в память и проверяет их (обязательные поля, enum'ы из
`src.models.enums`, ссылки внутри наборов); любая ошибка — `SeedError`, база при этом не затрагивается.
2. `apply_seed` (`seed_lib/apply.py`) применяет набор к БД шагами в порядке зависимостей: upsert по натуральному
ключу, без удалений. Исключение — матрица `role_permission`: целиком загружается только в пустую таблицу; в
заполненную — лишь пары ролей и прав, вставленных этим же запуском, остальное пропускается (в отчёте —
«пропущено»), чтобы не вернуть права, снятые администратором (issue #9 п.7, ревью R-3).
План счетов (`core/chart_of_accounts.csv`, design D-9) — ДОПУЩЕНИЕ OQ-47: полного плана 1С клиента нет, в файле
Приказ Минфина № 94н: 62 активных синтетических счёта (01–05, 07–11, 14–16, 19–21, 23, 25, 26, 28, 29, 40–46,
50–52, 55, 57–60, 62, 63, 66–71, 73, 75–77, 79–84, 86, 90, 91, 94, 96–99; номера 06, 12, 13, 17, 18, 22, 24, 27,
30–39, 47–49, 53, 54, 56, 61, 64, 65, 72, 74, 78, 85, 87–89, 92, 93, 95 в плане отсутствуют), забалансовые 001–011
(`kind = A`, `is_off_balance = true`) и только те субсчета и подсубсчета типового 1С БП 3.0, на которые ссылаются §4/§6
правил и чек-лист; `kind` субсчетов наследуется от счёта.
Остальное добавляет администратор (ARCH-004). Реестр кодов для проверки ссылок правил и чек-листа —
`scripts.seed_lib.model.account_codes(data)`.
Правила элиминации ББ (`core/elimination_rules_v1.yaml`, §4, версия 1) и чек-лист (`core/required_document_rules.yaml`,
ASSUMPTION(OQ-46)): `seed_lib/loaders_rules.py` проверяет, что каждый счёт есть в плане счетов набора, ДО записи в БД;
`seed_lib/apply_rules.py` — upsert (составы счетов и чек-лист приводятся к файлу без удалений; ключи без уникального
индекса — в Python, NULL в `account_id` не плодит дубли). Общие помощники чтения — `seed_lib/readers.py`.
Dev-данные (`--dev`, `seed/dev/`): dev-ГК под документы fixtures (`company_groups.yaml`: ГК, ЮЛ/ИП, алиасы,
ГСК/ГСЗ; отключаются `--no-dev-groups` — пустые справочники для импорта ЕЦП), 2 региональных филиала
(`regional_offices.yaml`, upsert по `name`)
и по одному пользователю на каждую системную роль (`users.yaml`, `<роль>@armka.local`, upsert `app_user` по `email`,
`user_role` по (user_id, role_id)). Ссылки пользователей на роли и филиалы проверяются до записи. Без `--dev` ни
пользователи, ни dev-филиалы не загружаются. Dev-данные — только для локальной разработки и тестовых стендов: CLI
принимает `--dev` лишь при `DEBUG=true` (`check_dev_allowed`, OQ-60, issue #9), иначе — код возврата 2 до чтения
seed и подключения к БД. `run_seed(session, dev=True)` защиты не имеет (её вызывают тесты). Удалений нет: роль,
убранная из `users.yaml`, остаётся назначенной.
Расширение (задачи 13–15): поле в `SeedData` (`seed_lib/model.py`), читатель в `load_seed`, шаг в `apply_seed`.
"""
import argparse
import asyncio
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from scripts.seed_lib.apply import apply_seed
from scripts.seed_lib.loaders import DEFAULT_SEED_DIR, load_seed
from scripts.seed_lib.model import SeedError, SeedReport
if TYPE_CHECKING:
from src.core.config import Settings
__all__ = [
"DEFAULT_SEED_DIR",
"DEV_FORBIDDEN_MESSAGE",
"SeedError",
"SeedReport",
"apply_seed",
"check_dev_allowed",
"load_seed",
"main",
"run_seed",
]
DEV_FORBIDDEN_MESSAGE = "--dev доступен только при DEBUG=true (dev-пользователи не для прода)"
#: код возврата CLI при `--dev` без DEBUG (1 — ошибка файлов seed)
EXIT_DEV_FORBIDDEN = 2
class DevSeedForbiddenError(Exception):
"""`--dev` запрошен при `DEBUG=false`."""
def check_dev_allowed(settings: "Settings") -> str | None:
"""Можно ли грузить dev-данные: `None` — да, иначе текст отказа (dev-пользователи только при `DEBUG=true`)."""
return None if settings.DEBUG else DEV_FORBIDDEN_MESSAGE
async def run_seed(
session: AsyncSession, *, dev: bool = False, seed_dir: Path | None = None, dev_groups: bool | None = None
) -> SeedReport:
"""Загрузить и применить seed (решение #18 / R-5): валидация до записи, без commit.
`seed_dir` — каталог с `core/` (и `dev/`), по умолчанию `api/seed`; подменяется в тестах. `dev_groups=False` —
dev-данные без ГК (фикстуры тестов API начинают с пустых справочников).
"""
return await apply_seed(session, load_seed(seed_dir, dev=dev, dev_groups=dev_groups))
async def _run_cli(seed_dir: Path, *, dev: bool, dev_groups: bool) -> SeedReport:
# импорт настроек — только после проверки файлов seed: модуль конфигурации читает ENV_FILE при импорте,
# поэтому `--help` и ошибки валидации без `--dev` работают без env; `--dev` сначала сверяется с DEBUG
if dev:
from src.core.config import get_settings # noqa: PLC0415
if (refusal := check_dev_allowed(get_settings())) is not None:
raise DevSeedForbiddenError(refusal)
data = load_seed(seed_dir, dev=dev, dev_groups=dev_groups)
from src.core.config import settings # noqa: PLC0415
engine = create_async_engine(
settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": "dkb"}}
)
try:
async with async_sessionmaker(engine, expire_on_commit=False)() as session, session.begin():
return await apply_seed(session, data)
finally:
await engine.dispose()
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="python -m scripts.seed", description="Загрузка справочников продукта.")
parser.add_argument(
"--dev",
action="store_true",
help="дополнительно загрузить dev-данные из seed/dev/ (только для разработки, не для прода)",
)
parser.add_argument(
"--no-dev-groups",
action="store_true",
help="с --dev: не загружать dev-ГК из seed/dev/company_groups.yaml (пустые справочники для импорта ЕЦП)",
)
parser.add_argument("--seed-dir", type=Path, default=DEFAULT_SEED_DIR, help="каталог seed (по умолчанию api/seed)")
args = parser.parse_args(argv)
try:
report = asyncio.run(_run_cli(args.seed_dir, dev=args.dev, dev_groups=not args.no_dev_groups))
except DevSeedForbiddenError as error:
print(f"Ошибка seed: {error}", file=sys.stderr)
return EXIT_DEV_FORBIDDEN
except SeedError as error:
print(f"Ошибка seed: {error}", file=sys.stderr)
return 1
print(report.format())
return 0
if __name__ == "__main__":
sys.exit(main())