Seed common work order catalog
Some checks failed
ci / test (push) Has been cancelled

This commit is contained in:
VPN SaaS Dev
2026-05-16 11:24:02 +09:00
parent 3e406aeb22
commit 8aa6640308
2 changed files with 257 additions and 1 deletions

View File

@@ -0,0 +1,256 @@
"""seed common work order catalog
Revision ID: 202605160002
Revises: 202605160001
Create Date: 2026-05-16 16:30:00.000000
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "202605160002"
down_revision: str | None = "202605160001"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
SOURCE = "system_catalog_202605160002"
KOREAN = ["Hyundai", "Kia", "Genesis", "SsangYong", "KGM", "GM Korea", "Chevrolet Korea", "Renault Korea", "Samsung Motors"]
PREMIUM_IMPORT = ["BMW", "Mercedes-Benz", "Lexus", "Toyota", "MINI"]
ALL_IMPORT = KOREAN + PREMIUM_IMPORT
def _meta(
makes: list[str] | None = None,
*,
tiers: list[str] | None = None,
keywords: list[str] | None = None,
notes: str | None = None,
) -> dict:
data: dict[str, object] = {"source": SOURCE}
if makes:
data["applicable_makes"] = makes
if tiers:
data["quality_tiers"] = tiers
if keywords:
data["keywords"] = keywords
if notes:
data["notes"] = notes
return data
def _item(
*,
item_type: str,
title: str,
category: str,
unit: str,
default_quantity: int | float = 1,
work_type: str | None = None,
product_type: str | None = None,
brand: str | None = None,
sku: str | None = None,
volume: int | float | None = None,
viscosity: str | None = None,
specification: str | None = None,
description: str | None = None,
metadata_json: dict | None = None,
) -> dict:
return {
"service_center_id": None,
"item_type": item_type,
"title": title,
"category": category,
"description": description,
"work_type": work_type,
"product_type": product_type,
"brand": brand,
"sku": sku,
"unit": unit,
"default_quantity": default_quantity,
"default_unit_price": 0,
"volume": volume,
"viscosity": viscosity,
"specification": specification,
"metadata_json": metadata_json or _meta(ALL_IMPORT),
"is_active": True,
}
def _work(title: str, category: str, work_type: str = "maintenance", *, makes: list[str] | None = None, keywords: list[str] | None = None) -> dict:
return _item(
item_type="work",
title=title,
category=category,
work_type=work_type,
product_type=None,
unit="job",
metadata_json=_meta(makes or ALL_IMPORT, keywords=keywords),
)
def _product(
title: str,
category: str,
product_type: str,
*,
unit: str = "pcs",
quantity: int | float = 1,
brand: str | None = None,
sku: str | None = None,
volume: int | float | None = None,
viscosity: str | None = None,
specification: str | None = None,
makes: list[str] | None = None,
keywords: list[str] | None = None,
notes: str | None = None,
) -> dict:
return _item(
item_type="product",
title=title,
category=category,
work_type=None,
product_type=product_type,
brand=brand,
sku=sku,
unit=unit,
default_quantity=quantity,
volume=volume,
viscosity=viscosity,
specification=specification,
metadata_json=_meta(makes or ALL_IMPORT, tiers=["OEM", "premium", "aftermarket"], keywords=keywords, notes=notes),
)
def upgrade() -> None:
catalog = sa.table(
"work_order_catalog_items",
sa.column("service_center_id", sa.Integer()),
sa.column("item_type", sa.String()),
sa.column("title", sa.String()),
sa.column("category", sa.String()),
sa.column("description", sa.Text()),
sa.column("work_type", sa.String()),
sa.column("product_type", sa.String()),
sa.column("brand", sa.String()),
sa.column("sku", sa.String()),
sa.column("unit", sa.String()),
sa.column("default_quantity", sa.Numeric()),
sa.column("default_unit_price", sa.Numeric()),
sa.column("volume", sa.Numeric()),
sa.column("viscosity", sa.String()),
sa.column("specification", sa.String()),
sa.column("metadata_json", sa.JSON()),
sa.column("is_active", sa.Boolean()),
)
rows = [
_work("Замена воздушного фильтра двигателя", "filters", keywords=["air filter", "engine air filter"]),
_work("Замена салонного фильтра", "filters", keywords=["cabin filter", "air conditioner filter"]),
_work("Замена топливного фильтра", "filters", keywords=["fuel filter"]),
_work("Замена свечей зажигания", "ignition", keywords=["spark plug"]),
_work("Замена свечей накаливания", "ignition", work_type="repair", keywords=["glow plug"]),
_work("Замена передних тормозных колодок", "brakes", work_type="repair", keywords=["front pads"]),
_work("Замена задних тормозных колодок", "brakes", work_type="repair", keywords=["rear pads"]),
_work("Замена передних тормозных дисков", "brakes", work_type="repair", keywords=["front rotors"]),
_work("Замена задних тормозных дисков", "brakes", work_type="repair", keywords=["rear rotors"]),
_work("Замена тормозной жидкости", "brakes", keywords=["brake fluid", "bleeding"]),
_work("Замена антифриза", "cooling", keywords=["coolant"]),
_work("Замена масла АКПП", "transmission", keywords=["atf", "automatic transmission"]),
_work("Замена масла CVT", "transmission", keywords=["cvt fluid"]),
_work("Замена масла DCT", "transmission", keywords=["dct fluid", "dual clutch"]),
_work("Замена масла редуктора", "drivetrain", keywords=["differential oil", "gear oil"]),
_work("Замена масла раздаточной коробки", "drivetrain", keywords=["transfer case"]),
_work("Диагностика подвески", "suspension", work_type="diagnostics", keywords=["chassis inspection"]),
_work("Замена стойки стабилизатора", "suspension", work_type="repair", keywords=["stabilizer link"]),
_work("Замена амортизатора", "suspension", work_type="repair", keywords=["shock absorber", "strut"]),
_work("Замена рычага подвески", "suspension", work_type="repair", keywords=["control arm"]),
_work("Замена ступичного подшипника", "suspension", work_type="repair", keywords=["wheel bearing"]),
_work("Развал-схождение", "wheel_alignment", keywords=["alignment"]),
_work("Замена приводного ремня", "engine", work_type="repair", keywords=["serpentine belt"]),
_work("Замена ролика натяжителя ремня", "engine", work_type="repair", keywords=["belt tensioner"]),
_work("Замена комплекта ремня ГРМ", "engine", work_type="repair", makes=KOREAN + ["Toyota", "Lexus"], keywords=["timing belt"]),
_work("Замена комплекта цепи ГРМ", "engine", work_type="repair", keywords=["timing chain"]),
_work("Обслуживание кондиционера", "climate", keywords=["ac service", "freon"]),
_work("Заправка кондиционера", "climate", keywords=["ac recharge"]),
_work("Замена аккумулятора", "electrical", work_type="repair", keywords=["battery"]),
_work("Шиномонтаж и балансировка", "tires", keywords=["tire mounting", "balancing"]),
_product("Масляный фильтр Hyundai/Kia/Genesis", "filters", "part", brand="Hyundai/Kia", sku="SYS-FILTER-OIL-HKG", makes=["Hyundai", "Kia", "Genesis"], keywords=["oil filter"]),
_product("Масляный фильтр SsangYong/KGM", "filters", "part", brand="KGM", sku="SYS-FILTER-OIL-KGM", makes=["SsangYong", "KGM"], keywords=["oil filter"]),
_product("Масляный фильтр GM Korea/Chevrolet", "filters", "part", brand="GM Korea", sku="SYS-FILTER-OIL-GMK", makes=["GM Korea", "Chevrolet Korea"], keywords=["oil filter"]),
_product("Масляный фильтр Renault Korea/Samsung", "filters", "part", brand="Renault Korea", sku="SYS-FILTER-OIL-RKM", makes=["Renault Korea", "Samsung Motors"], keywords=["oil filter"]),
_product("Масляный фильтр BMW/MINI", "filters", "part", brand="BMW/MINI", sku="SYS-FILTER-OIL-BMW-MINI", makes=["BMW", "MINI"], keywords=["oil filter"]),
_product("Масляный фильтр Mercedes-Benz", "filters", "part", brand="Mercedes-Benz", sku="SYS-FILTER-OIL-MB", makes=["Mercedes-Benz"], keywords=["oil filter"]),
_product("Масляный фильтр Lexus/Toyota", "filters", "part", brand="Toyota/Lexus", sku="SYS-FILTER-OIL-TY-LX", makes=["Toyota", "Lexus"], keywords=["oil filter"]),
_product("Воздушный фильтр двигателя", "filters", "part", sku="SYS-FILTER-AIR", keywords=["air filter"]),
_product("Салонный фильтр", "filters", "part", sku="SYS-FILTER-CABIN", keywords=["cabin filter"]),
_product("Салонный фильтр угольный", "filters", "part", sku="SYS-FILTER-CABIN-CARBON", keywords=["cabin carbon filter"]),
_product("Топливный фильтр бензиновый", "filters", "part", sku="SYS-FILTER-FUEL-GAS", keywords=["fuel filter"]),
_product("Топливный фильтр дизельный", "filters", "part", sku="SYS-FILTER-FUEL-DIESEL", keywords=["diesel fuel filter"]),
_product("Моторное масло 0W-20 API SP / ILSAC GF-6", "engine_oil", "fluid", unit="l", quantity=4, sku="SYS-OIL-0W20-SP", viscosity="0W-20", specification="API SP / ILSAC GF-6", makes=KOREAN + ["Toyota", "Lexus"], keywords=["engine oil"]),
_product("Моторное масло 5W-30 API SP / ACEA A5/B5", "engine_oil", "fluid", unit="l", quantity=4, sku="SYS-OIL-5W30-A5B5", viscosity="5W-30", specification="API SP / ACEA A5/B5", makes=KOREAN, keywords=["engine oil"]),
_product("Моторное масло 5W-40 ACEA A3/B4", "engine_oil", "fluid", unit="l", quantity=4, sku="SYS-OIL-5W40-A3B4", viscosity="5W-40", specification="ACEA A3/B4", keywords=["engine oil"]),
_product("Моторное масло BMW Longlife-04 0W-30/5W-30", "engine_oil", "fluid", unit="l", quantity=5, brand="BMW", sku="SYS-OIL-BMW-LL04", viscosity="0W-30 / 5W-30", specification="BMW LL-04", makes=["BMW", "MINI"], keywords=["engine oil", "ll04"]),
_product("Моторное масло Mercedes-Benz 229.52 5W-30", "engine_oil", "fluid", unit="l", quantity=5, brand="Mercedes-Benz", sku="SYS-OIL-MB-22952", viscosity="5W-30", specification="MB 229.52", makes=["Mercedes-Benz"], keywords=["engine oil"]),
_product("Моторное масло Toyota/Lexus 0W-20", "engine_oil", "fluid", unit="l", quantity=4, brand="Toyota/Lexus", sku="SYS-OIL-TY-LX-0W20", viscosity="0W-20", specification="Toyota/Lexus 0W-20", makes=["Toyota", "Lexus"], keywords=["engine oil"]),
_product("ATF Hyundai/Kia SP-IV", "transmission_fluid", "fluid", unit="l", quantity=6, brand="Hyundai/Kia", sku="SYS-ATF-SP4", specification="SP-IV", makes=["Hyundai", "Kia", "Genesis"], keywords=["atf"]),
_product("ATF Hyundai/Kia SP-III", "transmission_fluid", "fluid", unit="l", quantity=6, brand="Hyundai/Kia", sku="SYS-ATF-SP3", specification="SP-III", makes=["Hyundai", "Kia"], keywords=["atf"]),
_product("DCTF Hyundai/Kia", "transmission_fluid", "fluid", unit="l", quantity=2, brand="Hyundai/Kia", sku="SYS-DCTF-HKG", specification="DCTF", makes=["Hyundai", "Kia", "Genesis"], keywords=["dct fluid"]),
_product("CVTF Hyundai/Kia", "transmission_fluid", "fluid", unit="l", quantity=6, brand="Hyundai/Kia", sku="SYS-CVTF-HKG", specification="CVTF", makes=["Hyundai", "Kia"], keywords=["cvt fluid"]),
_product("ATF Dexron VI GM Korea", "transmission_fluid", "fluid", unit="l", quantity=6, brand="GM", sku="SYS-ATF-DEXRON6", specification="Dexron VI", makes=["GM Korea", "Chevrolet Korea"], keywords=["atf"]),
_product("ATF/CVTF Renault Korea", "transmission_fluid", "fluid", unit="l", quantity=6, brand="Renault Korea", sku="SYS-ATF-CVT-RKM", specification="ATF/CVTF по VIN", makes=["Renault Korea", "Samsung Motors"], keywords=["atf", "cvt fluid"], notes="Подбирать точную спецификацию по VIN и типу КПП."),
_product("ATF BMW ZF 8HP", "transmission_fluid", "fluid", unit="l", quantity=7, brand="ZF/BMW", sku="SYS-ATF-ZF8HP", specification="ZF Lifeguard 8 / BMW 8HP", makes=["BMW", "MINI"], keywords=["atf", "zf 8hp"]),
_product("ATF Mercedes 7G-Tronic/9G-Tronic", "transmission_fluid", "fluid", unit="l", quantity=7, brand="Mercedes-Benz", sku="SYS-ATF-MB-7G-9G", specification="MB 236.x по VIN", makes=["Mercedes-Benz"], keywords=["atf"], notes="Подбирать точную спецификацию по VIN и коробке."),
_product("ATF Toyota/Lexus WS", "transmission_fluid", "fluid", unit="l", quantity=6, brand="Toyota/Lexus", sku="SYS-ATF-TOYOTA-WS", specification="Toyota WS", makes=["Toyota", "Lexus"], keywords=["atf"]),
_product("Масло редуктора 75W-90 GL-5", "gear_oil", "fluid", unit="l", quantity=1, sku="SYS-GEAR-75W90-GL5", viscosity="75W-90", specification="API GL-5", keywords=["gear oil", "differential"]),
_product("Антифриз LLC/OAT", "coolant", "fluid", unit="l", quantity=4, sku="SYS-COOLANT-LLC-OAT", specification="LLC/OAT", keywords=["coolant"]),
_product("Антифриз Hyundai/Kia Long Life Coolant", "coolant", "fluid", unit="l", quantity=4, brand="Hyundai/Kia", sku="SYS-COOLANT-HKG-LLC", specification="Long Life Coolant", makes=["Hyundai", "Kia", "Genesis"], keywords=["coolant"]),
_product("Антифриз BMW/MINI G48/G11", "coolant", "fluid", unit="l", quantity=4, brand="BMW/MINI", sku="SYS-COOLANT-BMW-G48", specification="G48/G11", makes=["BMW", "MINI"], keywords=["coolant"]),
_product("Антифриз Mercedes-Benz 325.x", "coolant", "fluid", unit="l", quantity=4, brand="Mercedes-Benz", sku="SYS-COOLANT-MB-325", specification="MB 325.x", makes=["Mercedes-Benz"], keywords=["coolant"]),
_product("Тормозная жидкость DOT 4 LV", "brake_fluid", "fluid", unit="l", quantity=1, sku="SYS-BRAKE-DOT4-LV", specification="DOT 4 LV", keywords=["brake fluid"]),
_product("Жидкость ГУР CHF 11S/202", "power_steering_fluid", "fluid", unit="l", quantity=1, sku="SYS-PSF-CHF", specification="CHF 11S/202", makes=["BMW", "Mercedes-Benz", "MINI"], keywords=["power steering fluid"]),
_product("Передние тормозные колодки", "brakes", "part", sku="SYS-BRAKE-PADS-FRONT", keywords=["front brake pads"]),
_product("Задние тормозные колодки", "brakes", "part", sku="SYS-BRAKE-PADS-REAR", keywords=["rear brake pads"]),
_product("Передние тормозные диски", "brakes", "part", quantity=2, sku="SYS-BRAKE-ROTORS-FRONT", keywords=["front brake rotors"]),
_product("Задние тормозные диски", "brakes", "part", quantity=2, sku="SYS-BRAKE-ROTORS-REAR", keywords=["rear brake rotors"]),
_product("Датчик износа тормозных колодок BMW/MINI", "brakes", "part", brand="BMW/MINI", sku="SYS-BRAKE-SENSOR-BMW-MINI", makes=["BMW", "MINI"], keywords=["brake pad wear sensor"]),
_product("Датчик износа тормозных колодок Mercedes-Benz", "brakes", "part", brand="Mercedes-Benz", sku="SYS-BRAKE-SENSOR-MB", makes=["Mercedes-Benz"], keywords=["brake pad wear sensor"]),
_product("Свеча зажигания иридиевая", "ignition", "part", quantity=4, sku="SYS-SPARK-IRIDIUM", keywords=["spark plug"]),
_product("Свеча зажигания платиновая", "ignition", "part", quantity=4, sku="SYS-SPARK-PLATINUM", keywords=["spark plug"]),
_product("Свеча накаливания дизельная", "ignition", "part", quantity=4, sku="SYS-GLOW-PLUG", keywords=["glow plug"]),
_product("Катушка зажигания", "ignition", "part", sku="SYS-IGNITION-COIL", keywords=["ignition coil"]),
_product("Аккумулятор AGM 70Ah", "electrical", "part", sku="SYS-BATTERY-AGM-70", specification="AGM 70Ah", keywords=["battery"]),
_product("Аккумулятор AGM 80Ah", "electrical", "part", sku="SYS-BATTERY-AGM-80", specification="AGM 80Ah", keywords=["battery"]),
_product("Аккумулятор AGM 95Ah", "electrical", "part", sku="SYS-BATTERY-AGM-95", specification="AGM 95Ah", keywords=["battery"]),
_product("Аккумулятор EFB 60Ah", "electrical", "part", sku="SYS-BATTERY-EFB-60", specification="EFB 60Ah", makes=KOREAN, keywords=["battery"]),
_product("Аккумулятор EFB 70Ah", "electrical", "part", sku="SYS-BATTERY-EFB-70", specification="EFB 70Ah", makes=KOREAN, keywords=["battery"]),
_product("Стойка стабилизатора передняя", "suspension", "part", sku="SYS-SUSP-LINK-FRONT", keywords=["stabilizer link"]),
_product("Стойка стабилизатора задняя", "suspension", "part", sku="SYS-SUSP-LINK-REAR", keywords=["stabilizer link"]),
_product("Амортизатор передний", "suspension", "part", quantity=2, sku="SYS-SHOCK-FRONT", keywords=["front shock", "strut"]),
_product("Амортизатор задний", "suspension", "part", quantity=2, sku="SYS-SHOCK-REAR", keywords=["rear shock"]),
_product("Рычаг передней подвески", "suspension", "part", sku="SYS-CONTROL-ARM-FRONT", keywords=["control arm"]),
_product("Сайлентблок рычага", "suspension", "part", sku="SYS-BUSHING-CONTROL-ARM", keywords=["bushing"]),
_product("Шаровая опора", "suspension", "part", sku="SYS-BALL-JOINT", keywords=["ball joint"]),
_product("Ступичный подшипник", "suspension", "part", sku="SYS-WHEEL-BEARING", keywords=["wheel bearing"]),
_product("Приводной ремень", "engine", "part", sku="SYS-SERPENTINE-BELT", keywords=["serpentine belt"]),
_product("Ролик натяжителя приводного ремня", "engine", "part", sku="SYS-BELT-TENSIONER", keywords=["belt tensioner"]),
_product("Комплект ремня ГРМ", "engine", "part", sku="SYS-TIMING-BELT-KIT", makes=KOREAN + ["Toyota", "Lexus"], keywords=["timing belt kit"]),
_product("Комплект цепи ГРМ", "engine", "part", sku="SYS-TIMING-CHAIN-KIT", keywords=["timing chain kit"]),
_product("Фреон R134a", "climate", "consumable", unit="g", quantity=500, sku="SYS-AC-R134A", specification="R134a", keywords=["freon", "refrigerant"]),
_product("Фреон R1234yf", "climate", "consumable", unit="g", quantity=500, sku="SYS-AC-R1234YF", specification="R1234yf", keywords=["freon", "refrigerant"]),
_product("Масло компрессора кондиционера PAG", "climate", "fluid", unit="ml", quantity=100, sku="SYS-AC-PAG-OIL", specification="PAG", keywords=["ac compressor oil"]),
_product("Щетка стеклоочистителя 450 мм", "wipers", "part", sku="SYS-WIPER-450", specification="450 мм", keywords=["wiper blade"]),
_product("Щетка стеклоочистителя 500 мм", "wipers", "part", sku="SYS-WIPER-500", specification="500 мм", keywords=["wiper blade"]),
_product("Щетка стеклоочистителя 550 мм", "wipers", "part", sku="SYS-WIPER-550", specification="550 мм", keywords=["wiper blade"]),
_product("Щетка стеклоочистителя 600 мм", "wipers", "part", sku="SYS-WIPER-600", specification="600 мм", keywords=["wiper blade"]),
_product("Щетка стеклоочистителя 650 мм", "wipers", "part", sku="SYS-WIPER-650", specification="650 мм", keywords=["wiper blade"]),
]
op.bulk_insert(catalog, rows)
def downgrade() -> None:
op.execute(sa.text("DELETE FROM work_order_catalog_items WHERE metadata_json ->> 'source' = :source").bindparams(source=SOURCE))

View File

@@ -222,7 +222,7 @@ function catalogOptions(workOrder, itemType) {
const suggestions = itemType === "product" ? (catalog.vehicle_suggestions || []) : []; const suggestions = itemType === "product" ? (catalog.vehicle_suggestions || []) : [];
return [...catalogItems, ...suggestions].map((item) => { return [...catalogItems, ...suggestions].map((item) => {
const key = registerCatalogOption(item); const key = registerCatalogOption(item);
const meta = [item.category, item.specification || item.sku].filter(Boolean).join(" · "); const meta = [item.brand, item.category, item.specification || item.sku].filter(Boolean).join(" · ");
return `<option value="${escapeHtml(key)}">${escapeHtml(item.title)}${meta ? ` · ${escapeHtml(meta)}` : ""}</option>`; return `<option value="${escapeHtml(key)}">${escapeHtml(item.title)}${meta ? ` · ${escapeHtml(meta)}` : ""}</option>`;
}).join(""); }).join("");
} }