6fbff2fe76
Punto 2 del documento modifiche del 28/07, prima richiesta di Menoncin. Il sistema distingueva un task di misura da uno documentale deducendolo: se aveva quote era una misura, altrimenti una nota. Conseguenza: un task di misura a cui le quote non erano ancora state inserite veniva trattato come nota, e il sistema si comportava in modo diverso a seconda di quanto fosse completa la ricetta. Nuovo campo task_type su recipe_tasks (migrazione 007) con nota, misura e disegno, piu' xf_compare e camera_measure gia' nell'enum: allargare un enum MySQL piu' avanti e' un ALTER su tabella viva, e non costa nulla prevederli adesso. Il backfill riproduce la classificazione che era a schermo, cosi' nessuna ricetta cambia comportamento all'aggiornamento: i task con quote diventano 'measure'; quelli senza quote ma con un disegno allegato diventano 'drawing' e non 'note', perche' e' gia' cio' che mostravano - chiamarli note sarebbe stato l'unico punto in cui questa migrazione cambiava le carte in tavola. Il tipo viene copiato esplicitamente nel copy-on-write del versioning: una nuova versione che lo perdesse riclassificherebbe in silenzio tutti i task alla prima modifica di una ricetta. Il task "Technical Drawing" creato d'ufficio quando si carica un disegno su una ricetta senza task nasce come 'drawing'. Lato operatore decide ora il tipo, non il conteggio delle quote: fermo linea, fine produzione, avvio produzione e la barra di produzione seguono il tipo. Dove servono davvero delle quote da mostrare - elenco marker, tastierino, fine ciclo misura - resta anche il controllo che ce ne sia almeno una, e un task di misura ancora privo di quote lo dichiara invece di somigliare a una nota. Nella lista task compare il tipo, cosi' si vede prima di aprire. La 007 e' stata eseguita su SQLite usa e getta con tre righe costruite apposta - una con quote, una col solo disegno, una nota secca - e il backfill le classifica come atteso. Il backfill girera' sui dati reali del cliente, provarlo a mano non bastava. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
104 lines
4.3 KiB
Python
104 lines
4.3 KiB
Python
"""RecipeTask and RecipeSubtask models."""
|
|
from typing import TYPE_CHECKING, Optional
|
|
|
|
from sqlalchemy import (
|
|
DECIMAL, Enum, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from src.backend.database import Base
|
|
|
|
if TYPE_CHECKING:
|
|
from src.backend.models.orm.recipe import RecipeVersion
|
|
|
|
|
|
# What a task is, declared by whoever writes the recipe instead of guessed from its
|
|
# contents. The last two are not built yet; they are in the enum from the start
|
|
# because widening a MySQL enum later is an ALTER on a live table.
|
|
TASK_TYPES = ("note", "measure", "drawing", "xf_compare", "camera_measure")
|
|
|
|
# Types the operator screen treats as "there are quotes to take here".
|
|
MEASURING_TASK_TYPES = ("measure", "camera_measure")
|
|
|
|
|
|
class RecipeTask(Base):
|
|
__tablename__ = "recipe_tasks"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
version_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("recipe_versions.id"), nullable=False
|
|
)
|
|
order_index: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
# Declared, not deduced. The old rule - "has subtasks, therefore a measurement" -
|
|
# meant a measurement task whose quotes had not been entered yet behaved as a
|
|
# note, so the system acted differently depending on how finished the recipe was.
|
|
task_type: Mapped[str] = mapped_column(
|
|
Enum(*TASK_TYPES, name="task_type_enum"),
|
|
nullable=False,
|
|
default="note",
|
|
server_default="note",
|
|
index=True,
|
|
)
|
|
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
directive: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
file_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
|
file_type: Mapped[Optional[str]] = mapped_column(
|
|
Enum("image", "pdf", name="file_type_enum"), nullable=True
|
|
)
|
|
annotations_json: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
|
|
|
# Relationships
|
|
version: Mapped["RecipeVersion"] = relationship(back_populates="tasks")
|
|
subtasks: Mapped[list["RecipeSubtask"]] = relationship(
|
|
back_populates="task", cascade="all, delete-orphan", lazy="selectin"
|
|
)
|
|
|
|
__table_args__ = (
|
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"},
|
|
)
|
|
|
|
@property
|
|
def recipe_id(self) -> int | None:
|
|
"""Shortcut: recipe_id via the version relationship."""
|
|
if self.version:
|
|
return self.version.recipe_id
|
|
return None
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<RecipeTask #{self.order_index} '{self.title}'>"
|
|
|
|
|
|
class RecipeSubtask(Base):
|
|
__tablename__ = "recipe_subtasks"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
task_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("recipe_tasks.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
marker_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
description: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
measurement_type: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
|
|
|
# Tolerance values
|
|
nominal: Mapped[Optional[float]] = mapped_column(DECIMAL(12, 6), nullable=True)
|
|
utl: Mapped[Optional[float]] = mapped_column(DECIMAL(12, 6), nullable=True) # Upper Tolerance Limit
|
|
uwl: Mapped[Optional[float]] = mapped_column(DECIMAL(12, 6), nullable=True) # Upper Warning Limit
|
|
lwl: Mapped[Optional[float]] = mapped_column(DECIMAL(12, 6), nullable=True) # Lower Warning Limit
|
|
ltl: Mapped[Optional[float]] = mapped_column(DECIMAL(12, 6), nullable=True) # Lower Tolerance Limit
|
|
|
|
unit: Mapped[str] = mapped_column(String(20), nullable=False, default="mm")
|
|
image_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
|
|
|
# Relationships
|
|
task: Mapped["RecipeTask"] = relationship(back_populates="subtasks")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("task_id", "marker_number", name="uq_task_marker"),
|
|
Index("ix_subtask_task_id", "task_id"),
|
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"},
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<RecipeSubtask #{self.marker_number} '{self.description}'>"
|