Files
TieMeasureFlow/src/backend/models/orm/task.py
T
Adriano Dal Pastro 6fbff2fe76 feat(tasks): il tipo di un task si dichiara, non si deduce
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>
2026-07-28 18:15:18 +00:00

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}'>"