feat: Phase 5 — part counter update, replacement, replacement history
All checks were successful
Deploy to Production / deploy (push) Successful in 1m5s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m5s
This commit is contained in:
@@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from src.database.config import get_db
|
||||
from src.database.models import Machine, EquipmentPart, PartCounter
|
||||
from src.database.models import Machine, EquipmentPart, PartCounter, PartReplacementLog
|
||||
from src.auth.models import TokenData
|
||||
from src.auth.dependencies import require_auth, verify_tenant_access
|
||||
|
||||
@@ -37,6 +37,15 @@ class PartUpdate(BaseModel):
|
||||
counter_source: Optional[str] = None
|
||||
|
||||
|
||||
class CounterUpdate(BaseModel):
|
||||
value: float
|
||||
|
||||
|
||||
class ReplaceRequest(BaseModel):
|
||||
reason: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
def _format_ts(val) -> Optional[str]:
|
||||
if val is None:
|
||||
return None
|
||||
@@ -297,3 +306,154 @@ async def delete_part(
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "부품이 비활성화되었습니다."}
|
||||
|
||||
|
||||
@router.put("/api/{tenant_id}/parts/{part_id}/counter")
|
||||
async def update_counter(
|
||||
body: CounterUpdate,
|
||||
tenant_id: str = Path(...),
|
||||
part_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
|
||||
stmt = (
|
||||
select(EquipmentPart)
|
||||
.options(selectinload(EquipmentPart.counter))
|
||||
.where(
|
||||
EquipmentPart.id == part_id,
|
||||
EquipmentPart.tenant_id == tenant_id,
|
||||
EquipmentPart.is_active == True,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
part = result.scalar_one_or_none()
|
||||
if not part:
|
||||
raise HTTPException(status_code=404, detail="부품을 찾을 수 없습니다.")
|
||||
|
||||
counter = part.counter
|
||||
if not counter:
|
||||
raise HTTPException(status_code=404, detail="카운터를 찾을 수 없습니다.")
|
||||
|
||||
if body.value < 0:
|
||||
raise HTTPException(status_code=400, detail="카운터 값은 0 이상이어야 합니다.")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
counter.current_value = body.value
|
||||
counter.lifecycle_pct = (
|
||||
(body.value / float(part.lifecycle_limit)) * 100
|
||||
if float(part.lifecycle_limit) > 0
|
||||
else 0
|
||||
)
|
||||
counter.last_updated_at = now
|
||||
counter.version = (counter.version or 0) + 1
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(counter)
|
||||
|
||||
return _part_to_dict(part, counter)
|
||||
|
||||
|
||||
@router.post("/api/{tenant_id}/parts/{part_id}/replace")
|
||||
async def replace_part(
|
||||
tenant_id: str = Path(...),
|
||||
part_id: UUID = Path(...),
|
||||
body: Optional[ReplaceRequest] = None,
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
|
||||
stmt = (
|
||||
select(EquipmentPart)
|
||||
.options(selectinload(EquipmentPart.counter))
|
||||
.where(
|
||||
EquipmentPart.id == part_id,
|
||||
EquipmentPart.tenant_id == tenant_id,
|
||||
EquipmentPart.is_active == True,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
part = result.scalar_one_or_none()
|
||||
if not part:
|
||||
raise HTTPException(status_code=404, detail="부품을 찾을 수 없습니다.")
|
||||
|
||||
counter = part.counter
|
||||
if not counter:
|
||||
raise HTTPException(status_code=404, detail="카운터를 찾을 수 없습니다.")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
log = PartReplacementLog(
|
||||
tenant_id=tenant_id,
|
||||
equipment_part_id=part.id,
|
||||
replaced_by=UUID(current_user.user_id),
|
||||
replaced_at=now,
|
||||
counter_at_replacement=float(counter.current_value or 0),
|
||||
lifecycle_pct_at_replacement=float(counter.lifecycle_pct or 0),
|
||||
reason=body.reason if body else None,
|
||||
notes=body.notes if body else None,
|
||||
)
|
||||
db.add(log)
|
||||
|
||||
counter.current_value = 0
|
||||
counter.lifecycle_pct = 0
|
||||
counter.last_reset_at = now
|
||||
counter.last_updated_at = now
|
||||
counter.version = (counter.version or 0) + 1
|
||||
|
||||
part.installed_at = now
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(part)
|
||||
await db.refresh(counter)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "부품이 교체되었습니다.",
|
||||
"part": _part_to_dict(part, counter),
|
||||
}
|
||||
|
||||
|
||||
def _replacement_to_dict(log: PartReplacementLog) -> dict:
|
||||
return {
|
||||
"id": str(log.id),
|
||||
"equipment_part_id": str(log.equipment_part_id),
|
||||
"replaced_by": str(log.replaced_by) if log.replaced_by else None,
|
||||
"replaced_at": _format_ts(log.replaced_at),
|
||||
"counter_at_replacement": float(log.counter_at_replacement),
|
||||
"lifecycle_pct_at_replacement": float(log.lifecycle_pct_at_replacement),
|
||||
"reason": str(log.reason) if log.reason else None,
|
||||
"notes": str(log.notes) if log.notes else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/{tenant_id}/parts/{part_id}/replacements")
|
||||
async def list_replacements(
|
||||
tenant_id: str = Path(...),
|
||||
part_id: UUID = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
|
||||
part_stmt = select(EquipmentPart).where(
|
||||
EquipmentPart.id == part_id, EquipmentPart.tenant_id == tenant_id
|
||||
)
|
||||
part = (await db.execute(part_stmt)).scalar_one_or_none()
|
||||
if not part:
|
||||
raise HTTPException(status_code=404, detail="부품을 찾을 수 없습니다.")
|
||||
|
||||
stmt = (
|
||||
select(PartReplacementLog)
|
||||
.where(
|
||||
PartReplacementLog.equipment_part_id == part_id,
|
||||
PartReplacementLog.tenant_id == tenant_id,
|
||||
)
|
||||
.order_by(PartReplacementLog.replaced_at.desc())
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
logs = result.scalars().all()
|
||||
|
||||
return {"replacements": [_replacement_to_dict(log) for log in logs]}
|
||||
|
||||
Reference in New Issue
Block a user