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) MACHINES = { "spifox": [ { "name": "반제품 프레스 #1", "equipment_code": "SF-CUP-001", "model": "Cup Making Press 60T", "manufacturer": "SpiFox 자체설계", "location": "1동 반제품 라인", "area": "반제품 프레스", "criticality": "critical", "rated_capacity": "60T, UPH 520pcs", "power_rating": "15kW", "description": "알루미늄 슬러그 → CUP 형상 1차 성형. 딥드로잉 공법.", }, { "name": "반제품 프레스 #2", "equipment_code": "SF-CUP-002", "model": "Cup Making Press 60T", "manufacturer": "SpiFox 자체설계", "location": "1동 반제품 라인", "area": "반제품 프레스", "criticality": "critical", "rated_capacity": "60T, UPH 520pcs", "power_rating": "15kW", "description": "알루미늄 슬러그 → CUP 형상 1차 성형.", }, { "name": "완제품 프레스 #1", "equipment_code": "SF-FIN-001", "model": "Finish Press 80T", "manufacturer": "SpiFox 자체설계", "location": "2동 완제품 성형 라인 A", "area": "완제품 성형 A", "criticality": "critical", "rated_capacity": "80T, UPH 480pcs", "power_rating": "18.5kW", "description": "2차 딥드로잉 성형. 콘덴서 케이스 최종 형상.", }, { "name": "완제품 프레스 #2", "equipment_code": "SF-FIN-002", "model": "Finish Press 80T", "manufacturer": "SpiFox 자체설계", "location": "2동 완제품 성형 라인 A", "area": "완제품 성형 A", "criticality": "critical", "rated_capacity": "80T, UPH 480pcs", "power_rating": "18.5kW", "description": "2차 딥드로잉 성형.", }, { "name": "완제품 프레스 #3", "equipment_code": "SF-FIN-003", "model": "Finish Press 80T", "manufacturer": "SpiFox 자체설계", "location": "2동 완제품 성형 라인 B", "area": "완제품 성형 B", "criticality": "critical", "rated_capacity": "80T, UPH 480pcs", "power_rating": "18.5kW", }, { "name": "건조기 #1", "equipment_code": "SF-DRY-001", "model": "Hot Air Dryer 300", "manufacturer": "대한열기", "location": "1동 건조 구역", "area": "건조/세척", "criticality": "major", "rated_capacity": "반제품 건조 300kg/batch", "power_rating": "25kW", "description": "반제품 CUP 건조. 열풍 순환 방식.", }, { "name": "탈유기 #1", "equipment_code": "SF-DEG-001", "model": "Degreaser SC-500", "manufacturer": "세정산업", "location": "2동 후공정 라인", "area": "건조/세척", "criticality": "major", "rated_capacity": "500L 세척조", "power_rating": "12kW", "description": "성형유 제거. 초음파 탈유 방식.", }, { "name": "세척기 #1", "equipment_code": "SF-WSH-001", "model": "Ultrasonic Washer UW-800", "manufacturer": "세정산업", "location": "2동 후공정 라인", "area": "건조/세척", "criticality": "major", "rated_capacity": "800L 세척조", "power_rating": "15kW", "description": "탈유 후 초음파 세척.", }, { "name": "열처리로 #1", "equipment_code": "SF-HT-001", "model": "Annealing Furnace AF-1200", "manufacturer": "한국전기로", "location": "2동 열처리 구역", "area": "열처리", "criticality": "critical", "rated_capacity": "1200°C, 500kg/batch", "power_rating": "80kW", "description": "어닐링 열처리. 알루미늄 케이스 응력 제거.", }, { "name": "머신비전 검사기 #1", "equipment_code": "SF-VIS-001", "model": "AI Vision Inspector V3", "manufacturer": "SpiFox 자체개발", "location": "2동 검사 라인", "area": "검사/포장", "criticality": "critical", "rated_capacity": "검사속도 1,200pcs/min", "power_rating": "3kW", "description": "AI 머신러닝 기반 전수 외관검사. 치수/형상/결함 동시 검출.", }, { "name": "머신비전 검사기 #2", "equipment_code": "SF-VIS-002", "model": "AI Vision Inspector V3", "manufacturer": "SpiFox 자체개발", "location": "2동 검사 라인", "area": "검사/포장", "criticality": "critical", "rated_capacity": "검사속도 1,200pcs/min", "power_rating": "3kW", }, { "name": "자동포장기 #1", "equipment_code": "SF-PKG-001", "model": "Auto Packer AP-200", "manufacturer": "포장기계", "location": "2동 포장 구역", "area": "검사/포장", "criticality": "minor", "rated_capacity": "200box/h", "power_rating": "5kW", }, { "name": "구름다리 컨베이어", "equipment_code": "SF-CVY-001", "model": "Overbridge Conveyor OB-50", "manufacturer": "물류자동화", "location": "1동-2동 연결", "area": "물류/반송", "criticality": "major", "rated_capacity": "반제품 이송 50m", "power_rating": "7.5kW", "description": "1동(반제품)→2동(완제품) 반제품 및 빈 통 이송.", }, { "name": "AMR #1", "equipment_code": "SF-AMR-001", "model": "자율주행로봇 AGV-300", "manufacturer": "로봇솔루션", "location": "2동 물류 구역", "area": "물류/반송", "criticality": "minor", "rated_capacity": "적재 300kg", "power_rating": "배터리 48V", "description": "완제품 자동 반송. 성형→후공정→포장 경로.", }, ], "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": { "반제품 프레스 #1": [ ( "컷트핀 (Cut Pin)", "CP-CUP-01", "금형", "count", 150000, 80, "manual", 128000, ), ( "컷트메스 (Cut Die)", "CD-CUP-01", "금형", "count", 200000, 85, "manual", 165000, ), ("스트리퍼", "ST-CUP-01", "금형", "count", 300000, 80, "manual", 210000), ], "반제품 프레스 #2": [ ( "컷트핀 (Cut Pin)", "CP-CUP-02", "금형", "count", 150000, 80, "manual", 95000, ), ( "컷트메스 (Cut Die)", "CD-CUP-02", "금형", "count", 200000, 85, "manual", 120000, ), ("스트리퍼", "ST-CUP-02", "금형", "count", 300000, 80, "manual", 180000), ], "완제품 프레스 #1": [ ( "정형메스 (Forming Die)", "FD-FIN-01", "금형", "count", 100000, 80, "manual", 87000, ), ( "컷트핀 (Cut Pin)", "CP-FIN-01", "금형", "count", 120000, 80, "manual", 98000, ), ("펀치", "PH-FIN-01", "금형", "count", 250000, 85, "manual", 175000), ("성형유 필터", "OF-FIN-01", "소모품", "hours", 720, 80, "auto_time", 580), ], "완제품 프레스 #2": [ ( "정형메스 (Forming Die)", "FD-FIN-02", "금형", "count", 100000, 80, "manual", 62000, ), ( "컷트핀 (Cut Pin)", "CP-FIN-02", "금형", "count", 120000, 80, "manual", 73000, ), ("펀치", "PH-FIN-02", "금형", "count", 250000, 85, "manual", 140000), ], "완제품 프레스 #3": [ ( "정형메스 (Forming Die)", "FD-FIN-03", "금형", "count", 100000, 80, "manual", 45000, ), ( "컷트핀 (Cut Pin)", "CP-FIN-03", "금형", "count", 120000, 80, "manual", 52000, ), ("펀치", "PH-FIN-03", "금형", "count", 250000, 85, "manual", 95000), ], "건조기 #1": [ ( "히터 엘리먼트", "HE-DRY-01", "핵심부품", "hours", 8000, 85, "auto_time", 6500, ), ( "순환팬 베어링", "FB-DRY-01", "소모품", "hours", 15000, 80, "auto_time", 11000, ), ], "열처리로 #1": [ ("발열체", "HT-HT-01", "핵심부품", "hours", 10000, 80, "auto_time", 7200), ( "온도 센서 (TC)", "TC-HT-01", "계측", "hours", 5000, 90, "auto_time", 4200, ), ("단열재", "IN-HT-01", "소모품", "hours", 20000, 85, "auto_time", 14000), ], "머신비전 검사기 #1": [ ( "카메라 모듈", "CM-VIS-01", "핵심부품", "hours", 20000, 85, "auto_time", 8500, ), ("조명 LED", "LD-VIS-01", "소모품", "hours", 10000, 80, "auto_time", 7800), ("교정 인증서", "CAL-VIS-01", "인증", "date", 365, 80, "manual", 0), ], }, "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": "프레스 일일 점검", "subject_type": "equipment", "machine_name": "완제품 프레스 #1", "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": "반제품 프레스 일일 점검", "subject_type": "equipment", "machine_name": "반제품 프레스 #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": "equipment", "machine_name": "열처리로 #1", "schedule_type": "daily", "inspection_mode": "checklist", "items": [ { "name": "노내 온도", "category": "온도", "data_type": "numeric", "unit": "°C", "spec_min": 350, "spec_max": 420, "warning_min": 360, "warning_max": 410, }, { "name": "온도 균일도 (편차)", "category": "온도", "data_type": "numeric", "unit": "°C", "spec_min": 0, "spec_max": 10, "warning_max": 7, }, { "name": "배기 팬 작동 정상", "category": "기계", "data_type": "boolean", }, {"name": "도어 씰 상태", "category": "기계", "data_type": "boolean"}, { "name": "가스 분위기", "category": "분위기", "data_type": "select", "select_options": ["정상", "불안정", "이상"], }, ], }, { "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())