From 8aa66403083495d46fcffe90ac653aeab6bb56f8 Mon Sep 17 00:00:00 2001 From: VPN SaaS Dev Date: Sat, 16 May 2026 11:24:02 +0900 Subject: [PATCH] Seed common work order catalog --- .../202605160002_common_work_order_catalog.py | 256 ++++++++++++++++++ web/static/sto.js | 2 +- 2 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/202605160002_common_work_order_catalog.py diff --git a/alembic/versions/202605160002_common_work_order_catalog.py b/alembic/versions/202605160002_common_work_order_catalog.py new file mode 100644 index 0000000..32e6b15 --- /dev/null +++ b/alembic/versions/202605160002_common_work_order_catalog.py @@ -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)) diff --git a/web/static/sto.js b/web/static/sto.js index 2afec7b..f191907 100644 --- a/web/static/sto.js +++ b/web/static/sto.js @@ -222,7 +222,7 @@ function catalogOptions(workOrder, itemType) { const suggestions = itemType === "product" ? (catalog.vehicle_suggestions || []) : []; return [...catalogItems, ...suggestions].map((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 ``; }).join(""); }