"""task: in_loop / at_close / fields, tipo 'check', tabella task_results (#53 #54 #55) in_loop: il task fa parte del ciclo di produzione (prima: implicito dal tipo). at_close: il task 'note' si compila alla fine produzione. fields: campi extra del task 'note' (JSON). task_results: esito registrato da un task 'check' (OK/NOK) o 'note' (done). Revision ID: 018_task_loop_results Revises: 017_user_pin Create Date: 2026-10-07 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = '018_task_loop_results' down_revision: Union[str, None] = '017_user_pin' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None OLD_TYPES = ('note', 'measure', 'drawing', 'dxf_compare', 'camera_measure') NEW_TYPES = OLD_TYPES + ('check',) # stessi tipi di MEASURING_TASK_TYPES: fino a oggi erano l'unico ciclo possibile LOOP_TYPES = ('measure', 'camera_measure', 'dxf_compare') def upgrade() -> None: with op.batch_alter_table('recipe_tasks') as batch: batch.add_column(sa.Column( 'in_loop', sa.Boolean(), nullable=False, server_default=sa.false())) batch.add_column(sa.Column( 'at_close', sa.Boolean(), nullable=False, server_default=sa.false())) batch.add_column(sa.Column('fields', sa.JSON(), nullable=True)) # enum allargato come in 011: 'check' = Controllo OK/NOK batch.alter_column( 'task_type', existing_type=sa.Enum(*OLD_TYPES, name='task_type_enum'), type_=sa.Enum(*NEW_TYPES, name='task_type_enum'), existing_nullable=False, existing_server_default='note', ) # i task di misura erano gia' nel ciclo: lo restano recipe_tasks = sa.table( 'recipe_tasks', sa.column('task_type', sa.String), sa.column('in_loop', sa.Boolean)) op.execute( recipe_tasks.update() .where(recipe_tasks.c.task_type.in_(LOOP_TYPES)) .values(in_loop=True) ) op.create_table( 'task_results', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('task_id', sa.Integer(), sa.ForeignKey('recipe_tasks.id', ondelete='CASCADE'), nullable=False), sa.Column('version_id', sa.Integer(), sa.ForeignKey('recipe_versions.id'), nullable=False), sa.Column('production_run_id', sa.Integer(), sa.ForeignKey('production_runs.id', ondelete='SET NULL'), nullable=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id'), nullable=False), sa.Column('outcome', sa.Enum('ok', 'nok', 'done', name='task_outcome_enum'), nullable=False), sa.Column('note', sa.Text(), nullable=True), sa.Column('values', sa.JSON(), nullable=True), sa.Column('attachments', sa.JSON(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), mysql_engine='InnoDB', mysql_charset='utf8mb4', ) op.create_index('ix_task_results_task_id', 'task_results', ['task_id']) op.create_index('ix_task_results_version_id', 'task_results', ['version_id']) op.create_index( 'ix_task_results_production_run_id', 'task_results', ['production_run_id']) def downgrade() -> None: # il tipo 'check' non esiste prima di 018: se un task lo usa, tornare indietro # lo lascerebbe con un valore fuori enum bind = op.get_bind() used = bind.execute( sa.text("SELECT COUNT(*) FROM recipe_tasks WHERE task_type = 'check'") ).scalar() if used: raise RuntimeError( f"Downgrade 018 impossibile: {used} task usano il tipo 'check'. " "Cambiare il tipo o eliminare quei task prima di tornare indietro." ) op.drop_index('ix_task_results_production_run_id', table_name='task_results') op.drop_index('ix_task_results_version_id', table_name='task_results') op.drop_index('ix_task_results_task_id', table_name='task_results') op.drop_table('task_results') with op.batch_alter_table('recipe_tasks') as batch: batch.alter_column( 'task_type', existing_type=sa.Enum(*NEW_TYPES, name='task_type_enum'), type_=sa.Enum(*OLD_TYPES, name='task_type_enum'), existing_nullable=False, existing_server_default='note', ) batch.drop_column('fields') batch.drop_column('at_close') batch.drop_column('in_loop')