feat: Phase 3 — inspection templates (backend + frontend)
All checks were successful
Deploy to Production / deploy (push) Successful in 1m7s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m7s
- Add InspectionTemplate and InspectionTemplateItem models - Add 9 API endpoints for template CRUD and item management - Add Alembic migration for inspection_templates tables - Add 15 backend tests (39/39 total pass) - Add TemplateEditor component with item management UI - Add templates list, create, and edit pages - Add tab bar, template grid, form row CSS classes - Fix CORS middleware ordering in main.py - Move CORS middleware before router registration Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
533
src/api/templates.py
Normal file
533
src/api/templates.py
Normal file
@@ -0,0 +1,533 @@
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Path
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from src.database.config import get_db
|
||||
from src.database.models import InspectionTemplate, InspectionTemplateItem, Machine
|
||||
from src.auth.models import TokenData
|
||||
from src.auth.dependencies import require_auth, verify_tenant_access
|
||||
|
||||
router = APIRouter(prefix="/api/{tenant_id}/templates", tags=["templates"])
|
||||
|
||||
|
||||
VALID_SUBJECT_TYPES = ("equipment", "product")
|
||||
VALID_SCHEDULE_TYPES = ("daily", "weekly", "monthly", "yearly", "ad_hoc")
|
||||
VALID_INSPECTION_MODES = ("checklist", "measurement", "monitoring")
|
||||
VALID_DATA_TYPES = ("numeric", "boolean", "text", "select")
|
||||
|
||||
|
||||
class ItemCreate(BaseModel):
|
||||
name: str
|
||||
category: Optional[str] = None
|
||||
data_type: str
|
||||
unit: Optional[str] = None
|
||||
spec_min: Optional[float] = None
|
||||
spec_max: Optional[float] = None
|
||||
warning_min: Optional[float] = None
|
||||
warning_max: Optional[float] = None
|
||||
trend_window: Optional[int] = None
|
||||
select_options: Optional[list] = None
|
||||
equipment_part_id: Optional[str] = None
|
||||
is_required: bool = True
|
||||
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
data_type: Optional[str] = None
|
||||
unit: Optional[str] = None
|
||||
spec_min: Optional[float] = None
|
||||
spec_max: Optional[float] = None
|
||||
warning_min: Optional[float] = None
|
||||
warning_max: Optional[float] = None
|
||||
trend_window: Optional[int] = None
|
||||
select_options: Optional[list] = None
|
||||
equipment_part_id: Optional[str] = None
|
||||
is_required: Optional[bool] = None
|
||||
|
||||
|
||||
class TemplateCreate(BaseModel):
|
||||
name: str
|
||||
subject_type: str
|
||||
machine_id: Optional[str] = None
|
||||
product_code: Optional[str] = None
|
||||
schedule_type: str
|
||||
inspection_mode: str = "measurement"
|
||||
items: List[ItemCreate] = []
|
||||
|
||||
|
||||
class TemplateUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
subject_type: Optional[str] = None
|
||||
machine_id: Optional[str] = None
|
||||
product_code: Optional[str] = None
|
||||
schedule_type: Optional[str] = None
|
||||
inspection_mode: Optional[str] = None
|
||||
|
||||
|
||||
class ReorderRequest(BaseModel):
|
||||
item_ids: List[str]
|
||||
|
||||
|
||||
def _format_ts(val) -> Optional[str]:
|
||||
if val is None:
|
||||
return None
|
||||
return val.isoformat() if hasattr(val, "isoformat") else str(val)
|
||||
|
||||
|
||||
def _item_to_dict(item: InspectionTemplateItem) -> dict:
|
||||
return {
|
||||
"id": str(item.id),
|
||||
"template_id": str(item.template_id),
|
||||
"sort_order": item.sort_order,
|
||||
"name": str(item.name),
|
||||
"category": str(item.category) if item.category else None,
|
||||
"data_type": str(item.data_type),
|
||||
"unit": str(item.unit) if item.unit else None,
|
||||
"spec_min": float(item.spec_min) if item.spec_min is not None else None,
|
||||
"spec_max": float(item.spec_max) if item.spec_max is not None else None,
|
||||
"warning_min": float(item.warning_min)
|
||||
if item.warning_min is not None
|
||||
else None,
|
||||
"warning_max": float(item.warning_max)
|
||||
if item.warning_max is not None
|
||||
else None,
|
||||
"trend_window": int(item.trend_window)
|
||||
if item.trend_window is not None
|
||||
else None,
|
||||
"select_options": item.select_options,
|
||||
"equipment_part_id": str(item.equipment_part_id)
|
||||
if item.equipment_part_id
|
||||
else None,
|
||||
"is_required": bool(item.is_required),
|
||||
"created_at": _format_ts(item.created_at),
|
||||
}
|
||||
|
||||
|
||||
def _template_to_dict(
|
||||
t: InspectionTemplate, items_count: int = 0, include_items: bool = False
|
||||
) -> dict:
|
||||
result = {
|
||||
"id": str(t.id),
|
||||
"tenant_id": str(t.tenant_id),
|
||||
"name": str(t.name),
|
||||
"subject_type": str(t.subject_type),
|
||||
"machine_id": str(t.machine_id) if t.machine_id else None,
|
||||
"product_code": str(t.product_code) if t.product_code else None,
|
||||
"schedule_type": str(t.schedule_type),
|
||||
"inspection_mode": str(t.inspection_mode or "measurement"),
|
||||
"version": int(t.version or 1),
|
||||
"is_active": bool(t.is_active),
|
||||
"items_count": items_count,
|
||||
"created_at": _format_ts(t.created_at),
|
||||
"updated_at": _format_ts(t.updated_at),
|
||||
}
|
||||
if include_items:
|
||||
result["items"] = [_item_to_dict(i) for i in (t.items or [])]
|
||||
return result
|
||||
|
||||
|
||||
def _validate_subject_type(subject_type: str):
|
||||
if subject_type not in VALID_SUBJECT_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"subject_type은 {', '.join(VALID_SUBJECT_TYPES)} 중 하나여야 합니다.",
|
||||
)
|
||||
|
||||
|
||||
def _validate_schedule_type(schedule_type: str):
|
||||
if schedule_type not in VALID_SCHEDULE_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"schedule_type은 {', '.join(VALID_SCHEDULE_TYPES)} 중 하나여야 합니다.",
|
||||
)
|
||||
|
||||
|
||||
def _validate_inspection_mode(mode: str):
|
||||
if mode not in VALID_INSPECTION_MODES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"inspection_mode는 {', '.join(VALID_INSPECTION_MODES)} 중 하나여야 합니다.",
|
||||
)
|
||||
|
||||
|
||||
def _validate_data_type(data_type: str):
|
||||
if data_type not in VALID_DATA_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"data_type은 {', '.join(VALID_DATA_TYPES)} 중 하나여야 합니다.",
|
||||
)
|
||||
|
||||
|
||||
async def _get_template(
|
||||
db: AsyncSession, tenant_id: str, template_id: UUID, load_items: bool = False
|
||||
) -> InspectionTemplate:
|
||||
stmt = select(InspectionTemplate).where(
|
||||
InspectionTemplate.id == template_id,
|
||||
InspectionTemplate.tenant_id == tenant_id,
|
||||
InspectionTemplate.is_active == True,
|
||||
)
|
||||
if load_items:
|
||||
stmt = stmt.options(selectinload(InspectionTemplate.items))
|
||||
result = await db.execute(stmt)
|
||||
template = result.scalar_one_or_none()
|
||||
if not template:
|
||||
raise HTTPException(status_code=404, detail="검사 템플릿을 찾을 수 없습니다.")
|
||||
return template
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_templates(
|
||||
tenant_id: str = Path(...),
|
||||
subject_type: Optional[str] = None,
|
||||
schedule_type: Optional[str] = None,
|
||||
machine_id: Optional[str] = None,
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
InspectionTemplate,
|
||||
func.count(InspectionTemplateItem.id).label("items_count"),
|
||||
)
|
||||
.outerjoin(
|
||||
InspectionTemplateItem,
|
||||
InspectionTemplateItem.template_id == InspectionTemplate.id,
|
||||
)
|
||||
.where(
|
||||
InspectionTemplate.tenant_id == tenant_id,
|
||||
InspectionTemplate.is_active == True,
|
||||
)
|
||||
.group_by(InspectionTemplate.id)
|
||||
.order_by(InspectionTemplate.name)
|
||||
)
|
||||
|
||||
if subject_type:
|
||||
stmt = stmt.where(InspectionTemplate.subject_type == subject_type)
|
||||
if schedule_type:
|
||||
stmt = stmt.where(InspectionTemplate.schedule_type == schedule_type)
|
||||
if machine_id:
|
||||
stmt = stmt.where(InspectionTemplate.machine_id == UUID(machine_id))
|
||||
|
||||
result = await db.execute(stmt)
|
||||
rows = result.all()
|
||||
|
||||
return {"templates": [_template_to_dict(t, count) for t, count in rows]}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_template(
|
||||
body: TemplateCreate,
|
||||
tenant_id: str = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
_validate_subject_type(body.subject_type)
|
||||
_validate_schedule_type(body.schedule_type)
|
||||
_validate_inspection_mode(body.inspection_mode)
|
||||
|
||||
if body.subject_type == "equipment" and body.machine_id:
|
||||
machine_stmt = select(Machine).where(
|
||||
Machine.id == UUID(body.machine_id), Machine.tenant_id == tenant_id
|
||||
)
|
||||
if not (await db.execute(machine_stmt)).scalar_one_or_none():
|
||||
raise HTTPException(status_code=404, detail="설비를 찾을 수 없습니다.")
|
||||
|
||||
template = InspectionTemplate(
|
||||
tenant_id=tenant_id,
|
||||
name=body.name,
|
||||
subject_type=body.subject_type,
|
||||
machine_id=UUID(body.machine_id) if body.machine_id else None,
|
||||
product_code=body.product_code,
|
||||
schedule_type=body.schedule_type,
|
||||
inspection_mode=body.inspection_mode,
|
||||
)
|
||||
db.add(template)
|
||||
await db.flush()
|
||||
|
||||
for idx, item_data in enumerate(body.items):
|
||||
_validate_data_type(item_data.data_type)
|
||||
item = InspectionTemplateItem(
|
||||
template_id=template.id,
|
||||
sort_order=idx + 1,
|
||||
name=item_data.name,
|
||||
category=item_data.category,
|
||||
data_type=item_data.data_type,
|
||||
unit=item_data.unit,
|
||||
spec_min=item_data.spec_min,
|
||||
spec_max=item_data.spec_max,
|
||||
warning_min=item_data.warning_min,
|
||||
warning_max=item_data.warning_max,
|
||||
trend_window=item_data.trend_window,
|
||||
select_options=item_data.select_options,
|
||||
equipment_part_id=UUID(item_data.equipment_part_id)
|
||||
if item_data.equipment_part_id
|
||||
else None,
|
||||
is_required=item_data.is_required,
|
||||
)
|
||||
db.add(item)
|
||||
|
||||
await db.commit()
|
||||
|
||||
stmt = (
|
||||
select(InspectionTemplate)
|
||||
.options(selectinload(InspectionTemplate.items))
|
||||
.where(InspectionTemplate.id == template.id)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
created = result.scalar_one()
|
||||
|
||||
return _template_to_dict(created, len(created.items), include_items=True)
|
||||
|
||||
|
||||
@router.get("/{template_id}")
|
||||
async def get_template(
|
||||
tenant_id: str = Path(...),
|
||||
template_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
template = await _get_template(db, tenant_id, template_id, load_items=True)
|
||||
return _template_to_dict(template, len(template.items), include_items=True)
|
||||
|
||||
|
||||
@router.put("/{template_id}")
|
||||
async def update_template(
|
||||
body: TemplateUpdate,
|
||||
tenant_id: str = Path(...),
|
||||
template_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
template = await _get_template(db, tenant_id, template_id, load_items=True)
|
||||
|
||||
if body.subject_type is not None:
|
||||
_validate_subject_type(body.subject_type)
|
||||
template.subject_type = body.subject_type
|
||||
if body.schedule_type is not None:
|
||||
_validate_schedule_type(body.schedule_type)
|
||||
template.schedule_type = body.schedule_type
|
||||
if body.inspection_mode is not None:
|
||||
_validate_inspection_mode(body.inspection_mode)
|
||||
template.inspection_mode = body.inspection_mode
|
||||
if body.name is not None:
|
||||
template.name = body.name
|
||||
if body.machine_id is not None:
|
||||
template.machine_id = UUID(body.machine_id) if body.machine_id else None
|
||||
if body.product_code is not None:
|
||||
template.product_code = body.product_code
|
||||
|
||||
template.version = (template.version or 1) + 1
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(template)
|
||||
|
||||
return _template_to_dict(template, len(template.items), include_items=True)
|
||||
|
||||
|
||||
@router.delete("/{template_id}")
|
||||
async def delete_template(
|
||||
tenant_id: str = Path(...),
|
||||
template_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
template = await _get_template(db, tenant_id, template_id)
|
||||
|
||||
template.is_active = False
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "검사 템플릿이 비활성화되었습니다."}
|
||||
|
||||
|
||||
@router.post("/{template_id}/items")
|
||||
async def create_item(
|
||||
body: ItemCreate,
|
||||
tenant_id: str = Path(...),
|
||||
template_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
template = await _get_template(db, tenant_id, template_id, load_items=True)
|
||||
_validate_data_type(body.data_type)
|
||||
|
||||
max_order = max((i.sort_order for i in template.items), default=0)
|
||||
|
||||
item = InspectionTemplateItem(
|
||||
template_id=template.id,
|
||||
sort_order=max_order + 1,
|
||||
name=body.name,
|
||||
category=body.category,
|
||||
data_type=body.data_type,
|
||||
unit=body.unit,
|
||||
spec_min=body.spec_min,
|
||||
spec_max=body.spec_max,
|
||||
warning_min=body.warning_min,
|
||||
warning_max=body.warning_max,
|
||||
trend_window=body.trend_window,
|
||||
select_options=body.select_options,
|
||||
equipment_part_id=UUID(body.equipment_part_id)
|
||||
if body.equipment_part_id
|
||||
else None,
|
||||
is_required=body.is_required,
|
||||
)
|
||||
db.add(item)
|
||||
|
||||
template.version = (template.version or 1) + 1
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
|
||||
return _item_to_dict(item)
|
||||
|
||||
|
||||
@router.put("/{template_id}/items/reorder")
|
||||
async def reorder_items(
|
||||
body: ReorderRequest,
|
||||
tenant_id: str = Path(...),
|
||||
template_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
template = await _get_template(db, tenant_id, template_id, load_items=True)
|
||||
|
||||
item_map = {str(i.id): i for i in template.items}
|
||||
|
||||
if set(body.item_ids) != set(item_map.keys()):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="item_ids는 해당 템플릿의 모든 항목 ID를 포함해야 합니다.",
|
||||
)
|
||||
|
||||
for idx, item_id_str in enumerate(body.item_ids):
|
||||
item_map[item_id_str].sort_order = idx + 1
|
||||
|
||||
template.version = (template.version or 1) + 1
|
||||
|
||||
await db.commit()
|
||||
db.expire_all()
|
||||
|
||||
stmt = (
|
||||
select(InspectionTemplate)
|
||||
.options(selectinload(InspectionTemplate.items))
|
||||
.where(InspectionTemplate.id == template_id)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
refreshed = result.scalar_one()
|
||||
|
||||
return _template_to_dict(refreshed, len(refreshed.items), include_items=True)
|
||||
|
||||
|
||||
@router.put("/{template_id}/items/{item_id}")
|
||||
async def update_item(
|
||||
body: ItemUpdate,
|
||||
tenant_id: str = Path(...),
|
||||
template_id: UUID = Path(...),
|
||||
item_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
await _get_template(db, tenant_id, template_id)
|
||||
|
||||
stmt = select(InspectionTemplateItem).where(
|
||||
InspectionTemplateItem.id == item_id,
|
||||
InspectionTemplateItem.template_id == template_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
item = result.scalar_one_or_none()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="검사 항목을 찾을 수 없습니다.")
|
||||
|
||||
if body.name is not None:
|
||||
item.name = body.name
|
||||
if body.category is not None:
|
||||
item.category = body.category
|
||||
if body.data_type is not None:
|
||||
_validate_data_type(body.data_type)
|
||||
item.data_type = body.data_type
|
||||
if body.unit is not None:
|
||||
item.unit = body.unit
|
||||
if body.spec_min is not None:
|
||||
item.spec_min = body.spec_min
|
||||
if body.spec_max is not None:
|
||||
item.spec_max = body.spec_max
|
||||
if body.warning_min is not None:
|
||||
item.warning_min = body.warning_min
|
||||
if body.warning_max is not None:
|
||||
item.warning_max = body.warning_max
|
||||
if body.trend_window is not None:
|
||||
item.trend_window = body.trend_window
|
||||
if body.select_options is not None:
|
||||
item.select_options = body.select_options
|
||||
if body.equipment_part_id is not None:
|
||||
item.equipment_part_id = (
|
||||
UUID(body.equipment_part_id) if body.equipment_part_id else None
|
||||
)
|
||||
if body.is_required is not None:
|
||||
item.is_required = body.is_required
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
|
||||
return _item_to_dict(item)
|
||||
|
||||
|
||||
@router.delete("/{template_id}/items/{item_id}")
|
||||
async def delete_item(
|
||||
tenant_id: str = Path(...),
|
||||
template_id: UUID = Path(...),
|
||||
item_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
await _get_template(db, tenant_id, template_id)
|
||||
|
||||
stmt = select(InspectionTemplateItem).where(
|
||||
InspectionTemplateItem.id == item_id,
|
||||
InspectionTemplateItem.template_id == template_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
item = result.scalar_one_or_none()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="검사 항목을 찾을 수 없습니다.")
|
||||
|
||||
deleted_order = item.sort_order
|
||||
await db.delete(item)
|
||||
await db.flush()
|
||||
|
||||
remaining_stmt = (
|
||||
select(InspectionTemplateItem)
|
||||
.where(
|
||||
InspectionTemplateItem.template_id == template_id,
|
||||
InspectionTemplateItem.sort_order > deleted_order,
|
||||
)
|
||||
.order_by(InspectionTemplateItem.sort_order)
|
||||
)
|
||||
remaining_result = await db.execute(remaining_stmt)
|
||||
for remaining_item in remaining_result.scalars().all():
|
||||
remaining_item.sort_order -= 1
|
||||
|
||||
template_stmt = select(InspectionTemplate).where(
|
||||
InspectionTemplate.id == template_id
|
||||
)
|
||||
template = (await db.execute(template_stmt)).scalar_one()
|
||||
template.version = (template.version or 1) + 1
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "검사 항목이 삭제되었습니다."}
|
||||
@@ -14,7 +14,7 @@ from sqlalchemy import (
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB, TIMESTAMP
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
|
||||
from src.database.config import Base
|
||||
|
||||
@@ -164,3 +164,72 @@ class PartReplacementLog(Base):
|
||||
Index("ix_part_replacement_tenant_part", "tenant_id", "equipment_part_id"),
|
||||
Index("ix_part_replacement_tenant_date", "tenant_id", "replaced_at"),
|
||||
)
|
||||
|
||||
|
||||
class InspectionTemplate(Base):
|
||||
__tablename__ = "inspection_templates"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(String(50), ForeignKey("tenants.id"), nullable=False)
|
||||
name = Column(String(200), nullable=False)
|
||||
subject_type = Column(String(20), nullable=False) # equipment | product
|
||||
machine_id = Column(UUID(as_uuid=True), ForeignKey("machines.id"), nullable=True)
|
||||
product_code = Column(String(50), nullable=True)
|
||||
schedule_type = Column(
|
||||
String(20), nullable=False
|
||||
) # daily | weekly | monthly | yearly | ad_hoc
|
||||
inspection_mode = Column(
|
||||
String(20), default="measurement"
|
||||
) # checklist | measurement | monitoring
|
||||
version = Column(Integer, default=1)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(TIMESTAMP(timezone=True), default=utcnow)
|
||||
updated_at = Column(TIMESTAMP(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
|
||||
tenant = relationship("Tenant")
|
||||
machine = relationship("Machine")
|
||||
items = relationship(
|
||||
"InspectionTemplateItem",
|
||||
back_populates="template",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="InspectionTemplateItem.sort_order",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_templates_tenant_subject", "tenant_id", "subject_type"),
|
||||
Index("ix_templates_tenant_machine", "tenant_id", "machine_id"),
|
||||
)
|
||||
|
||||
|
||||
class InspectionTemplateItem(Base):
|
||||
__tablename__ = "inspection_template_items"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
template_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("inspection_templates.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
sort_order = Column(Integer, nullable=False)
|
||||
name = Column(String(200), nullable=False)
|
||||
category = Column(String(100), nullable=True)
|
||||
data_type = Column(String(20), nullable=False) # numeric | boolean | text | select
|
||||
unit = Column(String(20), nullable=True)
|
||||
spec_min = Column(Float, nullable=True)
|
||||
spec_max = Column(Float, nullable=True)
|
||||
warning_min = Column(Float, nullable=True)
|
||||
warning_max = Column(Float, nullable=True)
|
||||
trend_window = Column(Integer, nullable=True)
|
||||
select_options = Column(JSONB, nullable=True)
|
||||
equipment_part_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("equipment_parts.id"), nullable=True
|
||||
)
|
||||
is_required = Column(Boolean, default=True)
|
||||
created_at = Column(TIMESTAMP(timezone=True), default=utcnow)
|
||||
|
||||
template = relationship("InspectionTemplate", back_populates="items")
|
||||
equipment_part = relationship("EquipmentPart")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_template_items_template_order", "template_id", "sort_order"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user