df1727be73
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
105 lines
3.7 KiB
Python
105 lines
3.7 KiB
Python
"""Pydantic schemas for production runs and their events."""
|
|
from src.backend.models.api.tipi import UtcDateTime
|
|
from typing import Literal, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
|
|
|
|
class ProductionRunCreate(BaseModel):
|
|
station_code: str = Field(..., min_length=1, max_length=100)
|
|
recipe_id: int = Field(..., gt=0)
|
|
version_id: Optional[int] = Field(default=None, gt=0)
|
|
lot_number: Optional[str] = Field(default=None, max_length=100)
|
|
serial_number: Optional[str] = Field(default=None, max_length=100)
|
|
|
|
|
|
class ProductionEventResponse(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
id: int
|
|
event_type: str
|
|
user_id: int
|
|
supervisor_id: Optional[int]
|
|
task_id: Optional[int] = None
|
|
note: Optional[str]
|
|
created_at: UtcDateTime
|
|
|
|
|
|
class ProductionRunResponse(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
station_id: int
|
|
recipe_id: int
|
|
version_id: int
|
|
operator_id: int
|
|
lot_number: Optional[str]
|
|
serial_number: Optional[str]
|
|
status: str
|
|
measurement_interval_minutes: Optional[int]
|
|
next_measurement_at: Optional[UtcDateTime]
|
|
cycle_count: int
|
|
started_at: UtcDateTime
|
|
paused_at: Optional[UtcDateTime]
|
|
closed_at: Optional[UtcDateTime]
|
|
closed_by: Optional[int]
|
|
statistics_path: Optional[str] = None
|
|
|
|
# Derived server-side so every client agrees on the countdown regardless of
|
|
# clock skew. Negative once the interval has elapsed: how long the run has been
|
|
# overdue is a fact the operator must see, not deduce.
|
|
seconds_to_next_measurement: Optional[int] = None
|
|
overdue: bool = False
|
|
server_time: Optional[UtcDateTime] = None
|
|
|
|
# The measurement tasks of this run's version, in execution order. The client
|
|
# needs them to bring the operator back to the measurement when the interval
|
|
# expires, whichever screen they are on, and to know which task closes a cycle.
|
|
measurement_task_ids: list[int] = Field(default_factory=list)
|
|
# #53: task 'note' con at_close, da compilare prima della modale del capoturno.
|
|
close_task_ids: list[int] = Field(default_factory=list)
|
|
|
|
|
|
class ProductionRunWithEventsResponse(ProductionRunResponse):
|
|
events: list[ProductionEventResponse] = Field(default_factory=list)
|
|
|
|
|
|
class SupervisorAction(BaseModel):
|
|
"""Credentials of the supervisor authorising a line stop or a close."""
|
|
|
|
# #59: o username+password, oppure supervisor_user_id+supervisor_pin
|
|
supervisor_username: Optional[str] = Field(None, min_length=1)
|
|
supervisor_password: Optional[str] = Field(None, min_length=1)
|
|
supervisor_user_id: Optional[int] = Field(None, gt=0)
|
|
supervisor_pin: Optional[str] = Field(None, pattern=r"^[0-9]{4}$")
|
|
|
|
@model_validator(mode="after")
|
|
def _one_credential_pair(self):
|
|
by_password = self.supervisor_username and self.supervisor_password
|
|
by_pin = self.supervisor_user_id and self.supervisor_pin
|
|
if not (by_password or by_pin):
|
|
raise ValueError(
|
|
"supervisor credentials required: username+password or user_id+pin"
|
|
)
|
|
return self
|
|
note: Optional[str] = None
|
|
|
|
|
|
class CycleCompletePayload(BaseModel):
|
|
# Which measurement task was just finished. The interval restarts only on the
|
|
# last one of the recipe; omitting it closes the cycle outright.
|
|
task_id: Optional[int] = Field(default=None, gt=0)
|
|
note: Optional[str] = None
|
|
|
|
|
|
class RemeasurePayload(BaseModel):
|
|
"""The piece was turned over: measure it again inside the same cycle."""
|
|
|
|
task_id: Optional[int] = Field(default=None, gt=0)
|
|
note: Optional[str] = None
|
|
|
|
|
|
ProductionEventType = Literal[
|
|
"start", "cycle_completed", "task_measured", "remeasure",
|
|
"line_stop", "resume", "close",
|
|
]
|