ddf7788d77
Aggiunge POST /api/vision/execute: route l'immagine al worker di visione, mappa le uscite del grafo sulle quote e le salva con save_measurement - stesso verdetto, stesso gate del fuori tolleranza di ogni altra misura. Nuova tabella vision_results (una riga per acquisizione, non per quota) e input_method 'camera' su measurements. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
88 lines
3.4 KiB
Python
88 lines
3.4 KiB
Python
"""Measurement model for storing individual measurements."""
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from sqlalchemy import (
|
|
BigInteger, Boolean, DECIMAL, DateTime, Enum, ForeignKey,
|
|
Integer, String, func,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from src.backend.database import Base
|
|
|
|
|
|
class Measurement(Base):
|
|
__tablename__ = "measurements"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
subtask_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("recipe_subtasks.id"), nullable=False, index=True
|
|
)
|
|
version_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("recipe_versions.id"), nullable=False, index=True
|
|
)
|
|
measured_by: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("users.id"), nullable=False, index=True
|
|
)
|
|
|
|
# Measurement data
|
|
value: Mapped[float] = mapped_column(DECIMAL(12, 6), nullable=False)
|
|
pass_fail: Mapped[str] = mapped_column(
|
|
Enum("pass", "warning", "fail", name="pass_fail_enum"), nullable=False
|
|
)
|
|
deviation: Mapped[Optional[float]] = mapped_column(DECIMAL(12, 6), nullable=True)
|
|
|
|
# Traceability
|
|
lot_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
|
|
serial_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
|
|
|
|
# Input method
|
|
input_method: Mapped[str] = mapped_column(
|
|
Enum("usb_caliper", "manual", "camera", name="input_method_enum"),
|
|
nullable=False,
|
|
default="manual",
|
|
)
|
|
|
|
# The vision execution that produced this value, when there was one. Null for
|
|
# everything taken by caliper or typed by hand.
|
|
vision_result_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer, ForeignKey("vision_results.id"), nullable=True
|
|
)
|
|
|
|
# Time spent by the operator entering this value (client-reported)
|
|
input_duration_ms: Mapped[Optional[int]] = mapped_column(
|
|
Integer, nullable=True
|
|
)
|
|
|
|
# The production this was taken during, when there was one. Nullable: rows
|
|
# predating production runs, and measurements taken outside a production, have
|
|
# none. This is what makes "the measurements of this production" a query, which
|
|
# fine produzione needs to emit the statistics file.
|
|
production_run_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer,
|
|
ForeignKey("production_runs.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
|
|
# Who let an out-of-tolerance value stand, and when. Null on a value that is
|
|
# within tolerance, and null on one that is not and has not been authorised -
|
|
# which is exactly the state that stops the operator moving on to the next
|
|
# quote. Before this the supervisor's approval was a modal that closed: nothing
|
|
# recorded it, and nothing depended on it.
|
|
supervisor_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer, ForeignKey("users.id"), nullable=True, index=True
|
|
)
|
|
authorised_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
|
|
|
# Timestamp
|
|
measured_at: Mapped[datetime] = mapped_column(
|
|
DateTime, nullable=False, server_default=func.now(), index=True
|
|
)
|
|
|
|
# CSV sync flag
|
|
synced_to_csv: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Measurement subtask={self.subtask_id} value={self.value} {self.pass_fail}>"
|