diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index 0941659b..b1ad4661 100644 --- a/fusion_plating/fusion_plating_jobs/__manifest__.py +++ b/fusion_plating/fusion_plating_jobs/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Native Jobs', - 'version': '19.0.10.31.0', + 'version': '19.0.11.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'author': 'Nexa Systems Inc.', diff --git a/fusion_plating/fusion_plating_jobs/migrations/19.0.11.0.0/post-migrate.py b/fusion_plating/fusion_plating_jobs/migrations/19.0.11.0.0/post-migrate.py new file mode 100644 index 00000000..ba928123 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/migrations/19.0.11.0.0/post-migrate.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +"""Backfill new awaiting_cert / awaiting_ship states for mid-flight jobs. + +Spec: docs/superpowers/specs/2026-05-25-post-shop-cert-shipping-job-states-design.md + +Rules: + - in_progress + all steps terminal + draft cert exists → awaiting_cert + - in_progress + all steps terminal + no cert required → awaiting_ship + - done jobs LEFT ALONE — historically completed (already shipped) + +Idempotent: re-running on a fresh upgrade is a no-op because no +in_progress job will match the all-terminal predicate after the first +run. Pass 1 and Pass 2 are mutually exclusive (the cert-existence +sub-queries are inverses). +""" +import logging + +from odoo import api, SUPERUSER_ID + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + """Post-migrate entrypoint — called by Odoo after the module's + XML/Python loads on -u of fusion_plating_jobs.""" + + # ---- Pass 1: in_progress + all-terminal + draft cert → awaiting_cert + cr.execute(""" + UPDATE fp_job + SET state = 'awaiting_cert' + WHERE id IN ( + SELECT j.id + FROM fp_job j + JOIN fp_job_step s ON s.job_id = j.id + WHERE j.state = 'in_progress' + GROUP BY j.id + HAVING count(*) FILTER ( + WHERE s.state NOT IN ('done','skipped','cancelled') + ) = 0 + ) + AND EXISTS ( + SELECT 1 FROM fp_certificate c + WHERE c.x_fc_job_id = fp_job.id AND c.state = 'draft' + ); + """) + n_cert = cr.rowcount + _logger.info( + "post-migrate 19.0.11.0.0: %d jobs migrated to awaiting_cert", n_cert, + ) + + # ---- Pass 2: in_progress + all-terminal + no cert → awaiting_ship + cr.execute(""" + UPDATE fp_job + SET state = 'awaiting_ship' + WHERE id IN ( + SELECT j.id + FROM fp_job j + JOIN fp_job_step s ON s.job_id = j.id + WHERE j.state = 'in_progress' + GROUP BY j.id + HAVING count(*) FILTER ( + WHERE s.state NOT IN ('done','skipped','cancelled') + ) = 0 + ) + AND NOT EXISTS ( + SELECT 1 FROM fp_certificate c + WHERE c.x_fc_job_id = fp_job.id + AND c.state IN ('draft', 'issued') + ); + """) + n_ship = cr.rowcount + _logger.info( + "post-migrate 19.0.11.0.0: %d jobs migrated to awaiting_ship", n_ship, + ) + + # ---- Card_state recompute for affected rows (stored compute) ---- + if n_cert or n_ship: + env = api.Environment(cr, SUPERUSER_ID, {}) + affected = env['fp.job'].search([ + ('state', 'in', ('awaiting_cert', 'awaiting_ship')), + ]) + # Bust cache then read-to-recompute via @api.depends. + affected.invalidate_recordset(['card_state', 'mini_timeline_json']) + affected.mapped('card_state') + affected.mapped('mini_timeline_json') + _logger.info( + "post-migrate 19.0.11.0.0: card_state recomputed on %d jobs", + len(affected), + )