feat(fp.job): migration 19.0.11.0.0 — backfill new states (Task 21)

Idempotent post-migrate that moves mid-flight in_progress jobs whose
recipe steps are all terminal into the appropriate new state:
  - draft cert exists → awaiting_cert
  - no cert required  → awaiting_ship
done jobs left alone (historically completed, already shipped).

Card_state + mini_timeline_json recomputed for affected rows so the
plant kanban renders correctly on first page load.

Version bump 19.0.10.31.0 → 19.0.11.0.0 triggers the migration on -u.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-25 09:53:57 -04:00
parent 26a1086623
commit aab6b9275b
2 changed files with 92 additions and 1 deletions

View File

@@ -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.',

View File

@@ -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),
)