import asyncio import os import sys import uuid from datetime import datetime, timezone, timedelta sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv load_dotenv() from sqlalchemy import select from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from src.database.models import ( Base, User, Tenant, Machine, EquipmentPart, PartCounter, InspectionTemplate, InspectionTemplateItem, ) from src.auth.password import hash_password DATABASE_URL = os.getenv( "DATABASE_URL", "postgresql+asyncpg://factoryops:factoryops@localhost:5432/factoryops_v2", ) TENANTS = [ {"id": "spifox", "name": "SpiFox", "industry_type": "press_forming"}, {"id": "enkid", "name": "Enkid", "industry_type": "manufacturing"}, {"id": "alpet", "name": "Alpet", "industry_type": "chemical"}, ] SUPERADMIN = { "email": "admin@vexplor.com", "password": "qlalfqjsgh11", "name": "Super Admin", "role": "superadmin", "tenant_id": None, } TENANT_ADMINS = [ { "email": "admin@spifox.com", "password": "qlalfqjsgh11", "name": "SpiFox Admin", "role": "tenant_admin", "tenant_id": "spifox", }, { "email": "admin@enkid.com", "password": "qlalfqjsgh11", "name": "Enkid Admin", "role": "tenant_admin", "tenant_id": "enkid", }, { "email": "admin@alpet.com", "password": "qlalfqjsgh11", "name": "Alpet Admin", "role": "tenant_admin", "tenant_id": "alpet", }, ] # ─── 고객사별 설비 데이터 ─── NOW = datetime.now(timezone.utc) # ─── Spifox 설비 생성 (이천공장 304대 — Digital Twin DB 기준) ─── # CASE프레스 번호 목록 (P-1xx ~ P-9xx, 총 215대) _CASE_NUMBERS = ( list(range(101, 132)) # P-101 ~ P-131 (31) + list(range(201, 233)) # P-201 ~ P-232 (32) + list(range(301, 333)) # P-301 ~ P-332 (32) + list(range(401, 433)) # P-401 ~ P-432 (32) + list(range(501, 519)) # P-501 ~ P-518 (18) + list(range(601, 633)) # P-601 ~ P-632 (32) + list(range(701, 713)) # P-701 ~ P-712 (12) + list(range(801, 821)) # P-801 ~ P-820 (20) + [903, 904, 905, 906, 908, 910] # (6) ) SPIFOX_MACHINES: list[dict] = [] # ── CASE프레스 215대 ── for num in _CASE_NUMBERS: m: dict = { "name": f"CASE프레스 P-{num}", "equipment_code": f"SF-CASE-P{num}", "model": "Finish Press 80T", "manufacturer": "SpiFox", "location": "이천공장", "area": "CASE프레스", "criticality": "critical", "rated_capacity": "80T", "power_rating": "18.5kW", } if num == 101: m["description"] = "2차 딥드로잉 성형. 콘덴서 케이스 최종 형상." SPIFOX_MACHINES.append(m) # ── CUP프레스 32대 ── for i in range(1, 33): m = { "name": f"CUP프레스 {i}호기", "equipment_code": f"SF-CUP-{i:03d}", "model": "Cup Making Press 60T", "manufacturer": "SpiFox", "location": "이천공장", "area": "CUP프레스", "criticality": "critical", "rated_capacity": "60T, UPH 520pcs", "power_rating": "15kW", } if i == 1: m["description"] = "알루미늄 슬러그 → CUP 형상 1차 딥드로잉 성형." SPIFOX_MACHINES.append(m) # ── 컵핑 33대 (컵핑1~32호기 + 컵핑기 003호기) ── for i in range(1, 33): SPIFOX_MACHINES.append( { "name": f"컵핑{i}호기", "equipment_code": f"SF-CPN-{i:03d}", "model": "Cup Forming Press", "manufacturer": "SpiFox", "location": "이천공장", "area": "컵핑", "criticality": "critical", "rated_capacity": "컵핑 성형", "power_rating": "15kW", } ) SPIFOX_MACHINES.append( { "name": "컵핑기 003호기", "equipment_code": "SF-CPN-033", "model": "Cup Forming Press", "manufacturer": "SpiFox", "location": "이천공장", "area": "컵핑", "criticality": "critical", "rated_capacity": "컵핑 성형", "power_rating": "15kW", } ) # ── CUP건조설비 8대 ── for i in range(1, 9): m = { "name": f"CUP건조설비 {i}호기", "equipment_code": f"SF-DRY-{i:03d}", "model": "Hot Air Dryer 300", "manufacturer": "대한열기", "location": "이천공장", "area": "CUP건조설비", "criticality": "major", "rated_capacity": "반제품 건조 300kg/batch", "power_rating": "25kW", } if i == 1: m["description"] = "반제품 CUP 건조. 열풍 순환 방식." SPIFOX_MACHINES.append(m) # ── 건조 8대 ── for i in range(1, 9): SPIFOX_MACHINES.append( { "name": f"건조{i}호기", "equipment_code": f"SF-DRB-{i:03d}", "model": "Industrial Dryer", "manufacturer": "대한열기", "location": "이천공장", "area": "건조", "criticality": "major", "rated_capacity": "건조 300kg/batch", "power_rating": "25kW", } ) # ── EMS 6대 ── for i in range(1, 7): m = { "name": f"EMS {i}호기", "equipment_code": f"SF-EMS-{i:03d}", "model": "Electrified Monorail System", "manufacturer": "물류자동화", "location": "이천공장", "area": "EMS", "criticality": "major", "rated_capacity": "천장주행 모노레일", "power_rating": "5.5kW", } if i == 1: m["description"] = "반제품 공정 간 천장주행 이송 시스템(EMS)." SPIFOX_MACHINES.append(m) # ── 크레인 2대 ── SPIFOX_MACHINES.append( { "name": "CUP공급크레인", "equipment_code": "SF-CRN-001", "model": "Auto Warehouse Crane", "manufacturer": "물류자동화", "location": "이천공장", "area": "크레인", "criticality": "major", "rated_capacity": "자동창고 스태커 크레인", "power_rating": "11kW", "description": "CUP 반제품 자동창고 입고 크레인.", } ) SPIFOX_MACHINES.append( { "name": "CUP출고크레인", "equipment_code": "SF-CRN-002", "model": "Auto Warehouse Crane", "manufacturer": "물류자동화", "location": "이천공장", "area": "크레인", "criticality": "major", "rated_capacity": "자동창고 스태커 크레인", "power_rating": "11kW", "description": "CUP 반제품 자동창고 출고 크레인.", } ) assert len(SPIFOX_MACHINES) == 304, ( f"Expected 304 spifox machines, got {len(SPIFOX_MACHINES)}" ) MACHINES = { "spifox": SPIFOX_MACHINES, "enkid": [ { "name": "CNC 선반 #1", "equipment_code": "EK-CNC-001", "model": "두산 LYNX 220", "manufacturer": "두산공작기계", "location": "1공장 A라인", "area": "A라인", "criticality": "major", "rated_capacity": "최대 가공경 220mm", "power_rating": "15kW (주축 모터)", "description": "2축 CNC 선반. 알루미늄/스틸 가공용.", }, { "name": "프레스 500T", "equipment_code": "EK-PRS-001", "model": "현대위아 HF-500", "manufacturer": "현대위아", "location": "1공장 B라인", "area": "B라인", "criticality": "critical", "rated_capacity": "500톤", "power_rating": "37kW", }, { "name": "용접 로봇 #1", "equipment_code": "EK-WLD-001", "model": "화낙 ARC Mate 120", "manufacturer": "FANUC", "location": "2공장 용접 셀 1", "area": "용접 셀", "criticality": "major", "rated_capacity": "6축 120kg 가반하중", "power_rating": "6kVA", }, ], "alpet": [ { "name": "반응기 R-101", "equipment_code": "AP-RCT-101", "model": "Custom Reactor 5000L", "manufacturer": "한화솔루션 엔지니어링", "location": "1플랜트 반응 구역", "area": "반응 구역", "criticality": "critical", "rated_capacity": "5,000L (SUS316L)", "power_rating": "22kW (교반기)", "description": "교반식 배치 반응기. HCl 합성 공정.", }, { "name": "증류탑 D-201", "equipment_code": "AP-DST-201", "model": "Sulzer Packed Tower", "manufacturer": "Sulzer", "location": "1플랜트 분리 구역", "area": "분리 구역", "criticality": "critical", "rated_capacity": "처리량 2,000 kg/h", "power_rating": "리보일러 150kW", }, { "name": "열교환기 E-301", "equipment_code": "AP-HEX-301", "model": "Alfa Laval M10-BW", "manufacturer": "Alfa Laval", "location": "1플랜트 유틸리티", "area": "유틸리티", "criticality": "minor", "rated_capacity": "열전달 500kW", "power_rating": "펌프 7.5kW", }, ], } # 부품: (name, part_number, category, lifecycle_type, lifecycle_limit, alarm_threshold, counter_source, current_value) PARTS = { "spifox": { "CUP프레스 1호기": [ ( "컷트핀 (Cut Pin)", "CP-CUP-01", "금형", "count", 150000, 80, "manual", 128000, ), ( "컷트메스 (Cut Die)", "CD-CUP-01", "금형", "count", 200000, 85, "manual", 165000, ), ( "정형메스 (Forming Die)", "FD-CUP-01", "금형", "count", 300000, 80, "manual", 210000, ), ], "CASE프레스 P-101": [ ( "컷트핀 (Cut Pin)", "CP-FIN-01", "금형", "count", 120000, 80, "manual", 98000, ), ( "컷트메스 (Cut Die)", "CD-FIN-01", "금형", "count", 150000, 85, "manual", 112000, ), ( "정형메스 (Forming Die)", "FD-FIN-01", "금형", "count", 100000, 80, "manual", 87000, ), ], "CASE프레스 P-102": [ ( "컷트핀 (Cut Pin)", "CP-FIN-02", "금형", "count", 120000, 80, "manual", 73000, ), ( "컷트메스 (Cut Die)", "CD-FIN-02", "금형", "count", 150000, 85, "manual", 95000, ), ( "정형메스 (Forming Die)", "FD-FIN-02", "금형", "count", 100000, 80, "manual", 62000, ), ], "CASE프레스 P-103": [ ( "컷트핀 (Cut Pin)", "CP-FIN-03", "금형", "count", 120000, 80, "manual", 52000, ), ( "컷트메스 (Cut Die)", "CD-FIN-03", "금형", "count", 150000, 85, "manual", 68000, ), ( "정형메스 (Forming Die)", "FD-FIN-03", "금형", "count", 100000, 80, "manual", 45000, ), ], "CUP건조설비 1호기": [ ( "히터 엘리먼트", "HE-DRY-01", "핵심부품", "hours", 8000, 85, "auto_time", 6500, ), ( "순환팬 베어링", "FB-DRY-01", "소모품", "hours", 15000, 80, "auto_time", 11000, ), ], }, "enkid": { "CNC 선반 #1": [ ( "스핀들 베어링", "BRG-CNC-01", "핵심부품", "hours", 15000, 85, "auto_time", 12800, ), ( "절삭공구 홀더", "TH-CNC-01", "소모품", "count", 10000, 80, "manual", 7500, ), ( "볼스크류", "BS-CNC-01", "핵심부품", "hours", 20000, 90, "auto_time", 8000, ), ], "프레스 500T": [ ( "유압 실린더 씰", "HS-PRS-01", "소모품", "count", 50000, 85, "manual", 42000, ), ( "금형 상판", "MD-PRS-01", "핵심부품", "count", 100000, 80, "manual", 65000, ), ( "유압 펌프", "HP-PRS-01", "핵심부품", "hours", 10000, 85, "auto_time", 4200, ), ], "용접 로봇 #1": [ ("용접 토치", "WT-WLD-01", "소모품", "hours", 500, 80, "auto_time", 320), ( "와이어 피더", "WF-WLD-01", "소모품", "hours", 3000, 85, "auto_time", 1500, ), ( "안전 검사 인증", "CERT-WLD-01", "인증", "date", 180, 85, "manual", 0, ), ], }, "alpet": { "반응기 R-101": [ ( "교반기 임펠러", "IMP-RCT-01", "핵심부품", "hours", 20000, 85, "auto_time", 15500, ), ("기계적 씰", "MS-RCT-01", "소모품", "hours", 8000, 80, "auto_time", 6800), ("온도 센서", "TS-RCT-01", "계측", "hours", 5000, 90, "auto_time", 4600), ], "증류탑 D-201": [ ( "충전물 (패킹)", "PK-DST-01", "소모품", "hours", 30000, 85, "auto_time", 12000, ), ( "컨덴서 튜브", "CT-DST-01", "핵심부품", "hours", 25000, 80, "auto_time", 10000, ), ], "열교환기 E-301": [ ( "개스킷 세트", "GK-HEX-01", "소모품", "hours", 8000, 80, "auto_time", 7200, ), ( "플레이트 팩", "PP-HEX-01", "핵심부품", "hours", 40000, 85, "auto_time", 15000, ), ("압력 센서", "PS-HEX-01", "계측", "hours", 5000, 90, "auto_time", 2000), ( "내압 검사 인증", "CERT-HEX-01", "인증", "date", 730, 85, "manual", 0, ), ], }, } # ─── 고객사별 검사 템플릿 ─── TEMPLATES = { "spifox": [ { "name": "CASE프레스 일일 점검", "subject_type": "equipment", "machine_name": "CASE프레스 P-101", "schedule_type": "daily", "inspection_mode": "checklist", "items": [ { "name": "타발 압력", "category": "프레스", "data_type": "numeric", "unit": "T", "spec_min": 70, "spec_max": 85, "warning_min": 72, "warning_max": 83, }, { "name": "슬라이드 스트로크", "category": "프레스", "data_type": "numeric", "unit": "mm", "spec_min": 148, "spec_max": 152, "warning_min": 149, "warning_max": 151, }, { "name": "성형유 공급 정상", "category": "윤활", "data_type": "boolean", }, { "name": "금형 상태", "category": "금형", "data_type": "select", "select_options": ["양호", "주의", "교체필요"], }, { "name": "이상 소음/진동 여부", "category": "기계", "data_type": "boolean", }, { "name": "성형유 온도", "category": "윤활", "data_type": "numeric", "unit": "°C", "spec_min": 30, "spec_max": 50, "warning_max": 45, }, { "name": "안전장치 작동 확인", "category": "안전", "data_type": "boolean", }, { "name": "타발수 (현재)", "category": "생산", "data_type": "numeric", "unit": "회", }, ], }, { "name": "CUP프레스 일일 점검", "subject_type": "equipment", "machine_name": "CUP프레스 1호기", "schedule_type": "daily", "inspection_mode": "checklist", "items": [ { "name": "타발 압력", "category": "프레스", "data_type": "numeric", "unit": "T", "spec_min": 50, "spec_max": 65, "warning_min": 52, "warning_max": 63, }, { "name": "슬러그 공급 정상", "category": "소재", "data_type": "boolean", }, { "name": "금형 상태", "category": "금형", "data_type": "select", "select_options": ["양호", "주의", "교체필요"], }, { "name": "성형유 공급 정상", "category": "윤활", "data_type": "boolean", }, { "name": "이상 소음/진동 여부", "category": "기계", "data_type": "boolean", }, { "name": "안전장치 작동 확인", "category": "안전", "data_type": "boolean", }, ], }, { "name": "콘덴서 케이스 치수검사", "subject_type": "product", "product_code": "CASE-AL-1012", "schedule_type": "ad_hoc", "inspection_mode": "measurement", "items": [ { "name": "컷트핀 내경", "category": "치수", "data_type": "numeric", "unit": "mm", "spec_min": 9.98, "spec_max": 10.02, "warning_min": 9.99, "warning_max": 10.01, }, { "name": "컷트핀 외경", "category": "치수", "data_type": "numeric", "unit": "mm", "spec_min": 12.48, "spec_max": 12.52, "warning_min": 12.49, "warning_max": 12.51, }, { "name": "정형메스 내경", "category": "치수", "data_type": "numeric", "unit": "mm", "spec_min": 9.96, "spec_max": 10.04, "warning_min": 9.98, "warning_max": 10.02, }, { "name": "CR값", "category": "형상", "data_type": "numeric", "unit": "mm", "spec_min": 0.3, "spec_max": 0.5, "warning_min": 0.32, "warning_max": 0.48, }, { "name": "케이스 높이", "category": "치수", "data_type": "numeric", "unit": "mm", "spec_min": 11.9, "spec_max": 12.1, "warning_min": 11.95, "warning_max": 12.05, }, { "name": "표면 조도 (Ra)", "category": "표면", "data_type": "numeric", "unit": "μm", "spec_min": 0, "spec_max": 0.8, "warning_max": 0.6, }, {"name": "외관 결함 유무", "category": "외관", "data_type": "boolean"}, { "name": "결함 유형", "category": "외관", "data_type": "select", "select_options": [ "없음", "스크래치", "찍힘", "필름짤림", "오염", "기타", ], "is_required": False, }, { "name": "비고", "category": "기타", "data_type": "text", "is_required": False, }, ], }, ], "enkid": [ { "name": "CNC 선반 주간 점검", "subject_type": "equipment", "machine_name": "CNC 선반 #1", "schedule_type": "weekly", "inspection_mode": "checklist", "items": [ { "name": "스핀들 진동값", "category": "진동", "data_type": "numeric", "unit": "mm/s", "spec_min": 0, "spec_max": 4.5, "warning_max": 3.5, }, { "name": "절삭유 농도", "category": "유체", "data_type": "numeric", "unit": "%", "spec_min": 3, "spec_max": 8, "warning_min": 4, "warning_max": 7, }, {"name": "척 그립력 정상", "category": "기계", "data_type": "boolean"}, { "name": "윤활유 레벨", "category": "유체", "data_type": "select", "select_options": ["적정", "부족", "과다"], }, {"name": "이상 소음 여부", "category": "소음", "data_type": "boolean"}, { "name": "툴 마모 상태", "category": "공구", "data_type": "select", "select_options": ["양호", "주의", "교체필요"], }, ], }, { "name": "가공품 치수 검사", "subject_type": "product", "product_code": "SHAFT-AL-20", "schedule_type": "ad_hoc", "inspection_mode": "measurement", "items": [ { "name": "외경", "category": "치수", "data_type": "numeric", "unit": "mm", "spec_min": 19.98, "spec_max": 20.02, "warning_min": 19.99, "warning_max": 20.01, }, { "name": "길이", "category": "치수", "data_type": "numeric", "unit": "mm", "spec_min": 99.9, "spec_max": 100.1, "warning_min": 99.95, "warning_max": 100.05, }, { "name": "표면 거칠기 (Ra)", "category": "표면", "data_type": "numeric", "unit": "μm", "spec_min": 0, "spec_max": 1.6, "warning_max": 1.2, }, {"name": "외관 검사 합격", "category": "외관", "data_type": "boolean"}, ], }, ], "alpet": [ { "name": "반응기 일일 점검", "subject_type": "equipment", "machine_name": "반응기 R-101", "schedule_type": "daily", "inspection_mode": "checklist", "items": [ { "name": "반응 온도", "category": "온도", "data_type": "numeric", "unit": "°C", "spec_min": 180, "spec_max": 220, "warning_min": 185, "warning_max": 215, }, { "name": "반응 압력", "category": "압력", "data_type": "numeric", "unit": "bar", "spec_min": 2.0, "spec_max": 5.0, "warning_min": 2.5, "warning_max": 4.5, }, { "name": "교반 속도", "category": "기계", "data_type": "numeric", "unit": "RPM", "spec_min": 100, "spec_max": 300, "warning_min": 120, "warning_max": 280, }, { "name": "냉각수 유량 정상", "category": "유체", "data_type": "boolean", }, {"name": "누출 여부", "category": "안전", "data_type": "boolean"}, { "name": "씰 상태", "category": "기계", "data_type": "select", "select_options": ["양호", "주의", "교체필요"], }, ], }, { "name": "제품 농도 분석", "subject_type": "product", "product_code": "SOL-HCL-35", "schedule_type": "ad_hoc", "inspection_mode": "measurement", "items": [ { "name": "농도 (wt%)", "category": "농도", "data_type": "numeric", "unit": "wt%", "spec_min": 34.0, "spec_max": 36.0, "warning_min": 34.5, "warning_max": 35.5, }, { "name": "pH", "category": "농도", "data_type": "numeric", "unit": "pH", "spec_min": 0.1, "spec_max": 1.0, "warning_max": 0.8, }, { "name": "비중", "category": "물성", "data_type": "numeric", "unit": "g/mL", "spec_min": 1.16, "spec_max": 1.19, "warning_min": 1.165, "warning_max": 1.185, }, {"name": "색상 이상 유무", "category": "외관", "data_type": "boolean"}, { "name": "비고", "category": "기타", "data_type": "text", "is_required": False, }, ], }, ], } async def seed(): engine = create_async_engine(DATABASE_URL, echo=False) session_factory = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) async with session_factory() as db: # ── 1. Tenants ── for t in TENANTS: existing = await db.execute(select(Tenant).where(Tenant.id == t["id"])) if existing.scalar_one_or_none(): print(f" Tenant '{t['id']}' already exists, skipping") continue db.add(Tenant(**t)) print(f" + Tenant '{t['id']}' ({t['name']})") await db.commit() # ── 2. Users ── all_users = [SUPERADMIN] + TENANT_ADMINS for u in all_users: existing = await db.execute(select(User).where(User.email == u["email"])) if existing.scalar_one_or_none(): print(f" User '{u['email']}' already exists, skipping") continue db.add( User( id=uuid.uuid4(), email=u["email"], password_hash=hash_password(u["password"]), name=u["name"], role=u["role"], tenant_id=u["tenant_id"], ) ) print(f" + User '{u['email']}' (role={u['role']})") await db.commit() # ── 3. Machines ── machine_map = {} # (tenant_id, machine_name) → machine_id for tenant_id, machines in MACHINES.items(): for m in machines: existing = await db.execute( select(Machine).where( Machine.tenant_id == tenant_id, Machine.name == m["name"], ) ) row = existing.scalar_one_or_none() if row: print( f" Machine '{m['name']}' ({tenant_id}) already exists, skipping" ) machine_map[(tenant_id, m["name"])] = row.id continue machine_id = uuid.uuid4() install_date = NOW - timedelta(days=365 * 2) db.add( Machine( id=machine_id, tenant_id=tenant_id, name=m["name"], equipment_code=m["equipment_code"], model=m["model"], manufacturer=m.get("manufacturer"), installation_date=install_date, location=m.get("location"), area=m.get("area"), criticality=m.get("criticality", "major"), rated_capacity=m.get("rated_capacity"), power_rating=m.get("power_rating"), description=m.get("description"), ) ) machine_map[(tenant_id, m["name"])] = machine_id print(f" + Machine '{m['name']}' ({tenant_id})") await db.commit() # ── 4. Equipment Parts + Counters ── for tenant_id, machines_parts in PARTS.items(): for machine_name, parts in machines_parts.items(): machine_id = machine_map.get((tenant_id, machine_name)) if not machine_id: print(f" ! Machine '{machine_name}' not found for parts, skipping") continue for name, part_number, category, lt, ll, at, cs, cv in parts: existing = await db.execute( select(EquipmentPart).where( EquipmentPart.tenant_id == tenant_id, EquipmentPart.machine_id == machine_id, EquipmentPart.name == name, ) ) if existing.scalar_one_or_none(): print( f" Part '{name}' ({tenant_id}/{machine_name}) already exists, skipping" ) continue part_id = uuid.uuid4() db.add( EquipmentPart( id=part_id, tenant_id=tenant_id, machine_id=machine_id, name=name, part_number=part_number, category=category, lifecycle_type=lt, lifecycle_limit=ll, alarm_threshold=at, counter_source=cs, installed_at=NOW - timedelta(days=180), is_active=True, ) ) lifecycle_pct = round((cv / ll) * 100, 1) db.add( PartCounter( id=uuid.uuid4(), tenant_id=tenant_id, equipment_part_id=part_id, current_value=cv, lifecycle_pct=lifecycle_pct, last_reset_at=NOW - timedelta(days=180), last_updated_at=NOW - timedelta(hours=2), version=1, ) ) print( f" + Part '{name}' ({tenant_id}/{machine_name}) — {cv}/{ll} ({lifecycle_pct}%)" ) await db.commit() # ── 5. Inspection Templates ── for tenant_id, templates in TEMPLATES.items(): for tmpl in templates: existing = await db.execute( select(InspectionTemplate).where( InspectionTemplate.tenant_id == tenant_id, InspectionTemplate.name == tmpl["name"], ) ) if existing.scalar_one_or_none(): print( f" Template '{tmpl['name']}' ({tenant_id}) already exists, skipping" ) continue template_id = uuid.uuid4() machine_id_for_tmpl = None if tmpl.get("machine_name"): machine_id_for_tmpl = machine_map.get( (tenant_id, tmpl["machine_name"]) ) db.add( InspectionTemplate( id=template_id, tenant_id=tenant_id, name=tmpl["name"], subject_type=tmpl["subject_type"], machine_id=machine_id_for_tmpl, product_code=tmpl.get("product_code"), schedule_type=tmpl["schedule_type"], inspection_mode=tmpl["inspection_mode"], version=1, is_active=True, ) ) for idx, item in enumerate(tmpl["items"]): db.add( InspectionTemplateItem( id=uuid.uuid4(), template_id=template_id, sort_order=idx + 1, name=item["name"], category=item.get("category"), data_type=item["data_type"], unit=item.get("unit"), spec_min=item.get("spec_min"), spec_max=item.get("spec_max"), warning_min=item.get("warning_min"), warning_max=item.get("warning_max"), select_options=item.get("select_options"), is_required=item.get("is_required", True), ) ) print( f" + Template '{tmpl['name']}' ({tenant_id}) — {len(tmpl['items'])} items" ) await db.commit() await engine.dispose() print("\nSeed complete.") if __name__ == "__main__": print("Seeding FactoryOps v2 database...\n") asyncio.run(seed())