Split 49 modules/suites into independent git repos; untrack from monorepo
Each top-level module/suite folder is now its own private repo on GitHub (gsinghpal/<name>) and gitea (admin/<name>), with a fresh single initial commit. The monorepo no longer tracks them (added to .gitignore + git rm --cached); working-tree files are retained on disk and managed in their own repos. The monorepo keeps shared root files (CLAUDE.md, docs/, scripts/, tools/, AGENTS.md, WIP/obsolete dirs) and full history. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# 19.0.10.24.0 - Plant-view Shop Floor kanban redesign.
|
||||
# Backfill fp.job.step.last_activity_at from write_date so existing
|
||||
# in-progress steps don't immediately trip the S16 idle-warning gate
|
||||
# (8 hours since last activity) on first compute after deploy.
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
cr.execute("""
|
||||
UPDATE fp_job_step
|
||||
SET last_activity_at = write_date
|
||||
WHERE last_activity_at IS NULL
|
||||
""")
|
||||
cr.execute("SELECT count(*) FROM fp_job_step WHERE last_activity_at IS NULL")
|
||||
remaining = cr.fetchone()[0]
|
||||
if remaining:
|
||||
_logger.warning(
|
||||
"%d fp.job.step rows still have NULL last_activity_at after "
|
||||
"backfill (no write_date?). These will trip the idle gate "
|
||||
"on first compute.", remaining,
|
||||
)
|
||||
_logger.info("Backfilled last_activity_at on fp.job.step from write_date")
|
||||
@@ -1,155 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""19.0.10.25.0 - Template metadata backfill + recipe-node repointing.
|
||||
|
||||
Runs AFTER fusion_plating's pre-migrate 19.0.21.2.0 (which seeds
|
||||
kind.area_kind and activates derack/demask/gating). At this point:
|
||||
- All kinds have area_kind set.
|
||||
- blast / derack / demask / gating exist and are active.
|
||||
- XML data files have loaded (new templates exist).
|
||||
|
||||
This migration:
|
||||
1. Backfills code / description / icon / kind_id on the ~30 library
|
||||
templates seeded without metadata.
|
||||
2. Repoints existing recipe nodes from wrong kinds to correct ones
|
||||
using unambiguous name patterns.
|
||||
3. Recomputes area_kind on all fp.job.step rows.
|
||||
4. Recomputes active_step_id + card_state on in-flight jobs.
|
||||
|
||||
All phases idempotent - re-running -u is safe.
|
||||
|
||||
See docs/superpowers/specs/2026-05-24-shopfloor-live-step-fix-design.md.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from odoo.api import Environment, SUPERUSER_ID
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# (name : (code, icon, kind_code, description_snippet))
|
||||
TEMPLATE_BACKFILL = {
|
||||
'Acid Dip': ('ACID_DIP_STD', 'fa-flask', 'wet_process', 'Short acid immersion to activate the substrate before plating.'),
|
||||
'Air Dry': ('AIR_DRY_STD', 'fa-sun-o', 'wet_process', 'Air drying step between wet-line operations.'),
|
||||
'Bake': ('BAKE_STD', 'fa-fire', 'bake', 'Post-plate bake for hydrogen embrittlement relief.'),
|
||||
'Blasting': ('BLAST_STD', 'fa-bullseye', 'blast', 'Media or bead blasting to prepare the substrate.'),
|
||||
'Check Sulfamate Nickel Area': ('CHECK_SN_AREA', 'fa-search', 'inspect', 'Quick visual area check on the sulfamate nickel line.'),
|
||||
'Contract Review': ('CR_STD', 'fa-file-text-o', 'contract_review', 'QA-005 contract review gate. Required when the customer flag is on.'),
|
||||
'De-Masking': ('DEMASK_STD', 'fa-eraser', 'demask', 'Remove masking material after plating. Folds into De-Racking column.'),
|
||||
'DeRacking': ('DERACK_STD', 'fa-th', 'derack', 'Remove parts from racks for inspection / packaging.'),
|
||||
'Desmut': ('DESMUT_STD', 'fa-flask', 'wet_process', 'Remove smut from aluminium surfaces after etching.'),
|
||||
'Drying': ('DRYING_STD', 'fa-sun-o', 'wet_process', 'Drying step (oven or air) at the end of the wet line.'),
|
||||
'E-Nickel Plating': ('ENP_STD', 'fa-diamond', 'plate', 'Electroless nickel plate operation. Time and temp per recipe.'),
|
||||
'Electroclean': ('ECLEAN_STD', 'fa-bolt', 'wet_process', 'Anodic / cathodic electrocleaning step on the cleaning line.'),
|
||||
'Etch': ('ETCH_STD', 'fa-flask', 'wet_process', 'Chemical etching to prepare the substrate.'),
|
||||
'Final Inspection': ('FINAL_INSP_STD', 'fa-check-circle', 'final_inspect', 'Final visual + dimensional QA before packing.'),
|
||||
'HCl Activation': ('HCL_ACT_STD', 'fa-flask', 'wet_process', 'HCl activation dip prior to strike or plate.'),
|
||||
'Inspection': ('INSP_STD', 'fa-search', 'inspect', 'In-process inspection step.'),
|
||||
'Masking': ('MASK_STD', 'fa-paint-brush', 'mask', 'Apply masking to areas that should not be plated.'),
|
||||
'Nickel Strip (S-1)': ('NI_STRIP_S1', 'fa-undo', 'wet_process', 'Chemical strip of prior nickel deposit (rework path).'),
|
||||
'Nickel Strip - Steel Line': ('NI_STRIP_SL', 'fa-undo', 'wet_process', 'Chemical strip on the steel line (rework path).'),
|
||||
'Post-plate Inspection': ('POST_INSP_STD', 'fa-check-circle', 'inspect', 'Post-plate inspection - thickness sample + visual.'),
|
||||
'Pre-Measurements': ('PRE_MEAS_STD', 'fa-tachometer', 'inspect', 'Pre-process dimensional measurements (FAIR start point).'),
|
||||
'Racking': ('RACK_STD', 'fa-th', 'racking', 'Load parts onto racks for plating.'),
|
||||
'Ready for Plating': ('GATE_PLATE', 'fa-flag', 'gating', 'Gating step - parts staged ready for the plating line.'),
|
||||
'Ready for processing': ('GATE_PROC', 'fa-flag', 'gating', 'Generic gating step - parts staged ready for the next operation.'),
|
||||
'Rinse': ('RINSE_STD', 'fa-tint', 'wet_process', 'Rinse step between wet-line operations.'),
|
||||
'Shipping': ('SHIP_STD', 'fa-paper-plane', 'ship', 'Final shipping / hand-off to logistics.'),
|
||||
'Soak Clean': ('SOAK_CLEAN_STD', 'fa-bathtub', 'wet_process', 'Soak cleaning step at the start of the wet line.'),
|
||||
'Surface Activation': ('SURF_ACT_STD', 'fa-flask', 'wet_process', 'Surface activation dip prior to plate.'),
|
||||
'Water Break Test': ('WBF_TEST_STD', 'fa-tint', 'wet_process', 'Water-break test for surface cleanliness.'),
|
||||
'Zincate': ('ZINCATE_STD', 'fa-flask', 'wet_process', 'Zincate immersion on aluminium prior to plate.'),
|
||||
}
|
||||
|
||||
# (filter_sql, current_kind_code, new_kind_code, description)
|
||||
# current_kind_code=None means "any kind that isn't the target"
|
||||
NODE_REPOINTING = [
|
||||
("n.name = 'Blasting'", 'other', 'blast', 'Blasting -> blast'),
|
||||
("n.name ILIKE 'Ready %%'", None, 'gating', 'Ready For X -> gating'),
|
||||
# De-Masking: anchored ILIKE (must start with "De-Masking" or
|
||||
# "DeMasking") so we don't match "Ready For De-Masking" which the
|
||||
# earlier 'Ready %' rule already moved to gating. cur_code=None
|
||||
# catches both 'mask' (the 34 lazy/legacy ones) and 'other' (4
|
||||
# older nodes never reclassified after the demask kind was added).
|
||||
# The trailing AND k.code != %s safeguard skips already-correct rows.
|
||||
("(n.name ILIKE 'De-Masking%%' OR n.name ILIKE 'DeMasking%%')", None, 'demask', 'De-Masking -> demask'),
|
||||
("n.name = 'Scheduling'", 'other', 'gating', 'Scheduling -> gating'),
|
||||
("n.name ILIKE '%%Nickel Strip%%'", 'plate', 'wet_process', 'Nickel Strip -> wet_process'),
|
||||
("n.name ILIKE '%%Pre-Measurement%%' OR n.name ILIKE '%%Check Sulfamate%%'", 'other', 'inspect', 'Pre-Meas/Check Sulfamate -> inspect'),
|
||||
]
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
env = Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# Phase 1 - Template metadata backfill. Idempotent: only fills
|
||||
# NULL/empty fields, doesn't overwrite admin edits.
|
||||
Tpl = env['fp.step.template']
|
||||
Kind = env['fp.step.kind']
|
||||
fixed_tpl = 0
|
||||
for name, (code, icon, kind_code, desc) in TEMPLATE_BACKFILL.items():
|
||||
tpl = Tpl.search([('name', '=', name)], limit=1)
|
||||
if not tpl:
|
||||
continue
|
||||
vals = {}
|
||||
if not tpl.code:
|
||||
vals['code'] = code
|
||||
cur_desc = (tpl.description or '').strip()
|
||||
if cur_desc in ('', '<p><br></p>', '<p></p>'):
|
||||
vals['description'] = '<p>%s</p>' % desc
|
||||
if tpl.icon == 'fa-cog':
|
||||
vals['icon'] = icon
|
||||
kind = Kind.search([('code', '=', kind_code)], limit=1)
|
||||
if kind and tpl.kind_id.code != kind_code:
|
||||
vals['kind_id'] = kind.id
|
||||
if vals:
|
||||
tpl.write(vals)
|
||||
fixed_tpl += 1
|
||||
_logger.info(
|
||||
'[live-step-fix] template metadata backfilled: %s templates updated',
|
||||
fixed_tpl,
|
||||
)
|
||||
|
||||
# Phase 2 - Recipe node repointing. Idempotent: AND k.code != %s
|
||||
# ensures already-correct rows are skipped on re-run.
|
||||
for filter_sql, cur_code, new_code, desc in NODE_REPOINTING:
|
||||
params = [new_code]
|
||||
sql = (
|
||||
"UPDATE fusion_plating_process_node n "
|
||||
"SET kind_id = (SELECT id FROM fp_step_kind WHERE code = %s LIMIT 1) "
|
||||
"FROM fp_step_kind k "
|
||||
"WHERE n.kind_id = k.id "
|
||||
"AND (" + filter_sql + ")"
|
||||
)
|
||||
if cur_code is not None:
|
||||
sql += " AND k.code = %s"
|
||||
params.append(cur_code)
|
||||
sql += " AND k.code != %s"
|
||||
params.append(new_code)
|
||||
cr.execute(sql, params)
|
||||
_logger.info(
|
||||
'[live-step-fix] repointed %s nodes: %s',
|
||||
cr.rowcount, desc,
|
||||
)
|
||||
|
||||
# Phase 3 - Recompute area_kind on every fp.job.step row.
|
||||
steps = env['fp.job.step'].search([])
|
||||
if steps:
|
||||
steps._compute_area_kind()
|
||||
steps.flush_recordset(['area_kind'])
|
||||
_logger.info(
|
||||
'[live-step-fix] recomputed area_kind on %s steps', len(steps),
|
||||
)
|
||||
|
||||
# Phase 4 - Recompute active_step_id + card_state on in-flight jobs.
|
||||
jobs = env['fp.job'].search([
|
||||
('state', 'in', ('confirmed', 'in_progress')),
|
||||
])
|
||||
if jobs:
|
||||
jobs._compute_active_step_id()
|
||||
jobs._compute_card_state()
|
||||
jobs.flush_recordset(['active_step_id', 'card_state'])
|
||||
_logger.info(
|
||||
'[live-step-fix] recomputed active_step_id + card_state on %s jobs',
|
||||
len(jobs),
|
||||
)
|
||||
@@ -1,234 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""19.0.10.26.0 - Recipe cleanup + per-part clone delete.
|
||||
|
||||
Spec: docs/superpowers/specs/2026-05-24-recipe-cleanup-design.md
|
||||
|
||||
Phases (in order):
|
||||
1. Resequence recipe 3620 ENP-ALUM-BASIC operations + delete the
|
||||
duplicate empty ENP-Alum Line sub_process (id 4056).
|
||||
2. Backfill kind on all kind=other nodes via the extended
|
||||
fp_resolve_step_kind() resolver + RESOLVER_KIND_TO_ACTIVE_KIND
|
||||
translation.
|
||||
3. Delete all per-part clone recipes (name ILIKE '% - %').
|
||||
CASCADE handles child nodes; SET NULL handles fp.job /
|
||||
fp.job.step / fp.coating.config / fp.pricing.rule /
|
||||
fp.part.catalog references.
|
||||
4. Recompute fp.job.step.area_kind on all rows.
|
||||
5. Recompute fp.job.active_step_id + card_state on in-flight jobs.
|
||||
|
||||
All phases idempotent - re-running -u is safe.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from odoo.api import Environment, SUPERUSER_ID
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Recipe 3620's ops in the desired final order. Maps the existing node
|
||||
# id (as documented in the spec) to its target sequence. The user
|
||||
# decided mask-first-then-rack (matches the existing De-Masking step's
|
||||
# position between Plating and Bake; de-mask before de-rack would be
|
||||
# illogical).
|
||||
RECIPE_3620_RESEQUENCE = [
|
||||
# (node_id, new_sequence, expected_name)
|
||||
(3853, 10, 'Contract Review'),
|
||||
(3854, 20, 'Incoming Inspection (Standard)'),
|
||||
(3877, 30, 'Masking'),
|
||||
(3855, 40, 'Racking'),
|
||||
(3858, 50, 'Ready for processing'),
|
||||
(3859, 60, 'ENP-Alum Line'),
|
||||
(3861, 70, 'De-Masking'),
|
||||
(3864, 80, 'Oven baking'),
|
||||
(3867, 90, 'De-racking'),
|
||||
(4067, 100, 'Oven bake (Post de-rack)'),
|
||||
(3873, 110, 'Post-plate Inspection'),
|
||||
(3876, 120, 'Final Inspection'),
|
||||
]
|
||||
|
||||
# Empty duplicate ENP-Alum Line sub_process on recipe 3620 (no
|
||||
# children - the real one is id 3859 with E-Nickel Plating as child).
|
||||
RECIPE_3620_DUPLICATE_TO_DELETE = 4056
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
env = Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# ============================================================
|
||||
# Phase 1 - Resequence recipe 3620 + delete duplicate sub_process
|
||||
# ============================================================
|
||||
Node = env['fusion.plating.process.node']
|
||||
recipe_3620 = Node.browse(3620).exists()
|
||||
if not recipe_3620:
|
||||
_logger.warning(
|
||||
'[recipe-cleanup] Recipe 3620 ENP-ALUM-BASIC not found; '
|
||||
'skipping resequence phase'
|
||||
)
|
||||
else:
|
||||
# Resequence idempotently - only update if sequence differs.
|
||||
renumbered = 0
|
||||
for node_id, new_seq, expected_name in RECIPE_3620_RESEQUENCE:
|
||||
node = Node.browse(node_id).exists()
|
||||
if not node:
|
||||
_logger.warning(
|
||||
'[recipe-cleanup] Recipe 3620: expected node %s '
|
||||
'("%s") not found; skipping',
|
||||
node_id, expected_name,
|
||||
)
|
||||
continue
|
||||
if node.sequence != new_seq:
|
||||
# Skip autoclassify - we only touch sequence here.
|
||||
node.with_context(
|
||||
fp_skip_kind_autoclassify=True,
|
||||
).write({'sequence': new_seq})
|
||||
renumbered += 1
|
||||
_logger.info(
|
||||
'[recipe-cleanup] Recipe 3620: %s nodes resequenced',
|
||||
renumbered,
|
||||
)
|
||||
|
||||
# Delete the empty duplicate ENP-Alum Line sub_process.
|
||||
dup = Node.browse(RECIPE_3620_DUPLICATE_TO_DELETE).exists()
|
||||
if dup:
|
||||
if dup.child_ids:
|
||||
_logger.warning(
|
||||
'[recipe-cleanup] Duplicate sub_process %s has '
|
||||
'%s children - NOT deleting (safety check). '
|
||||
'Expected an empty node.',
|
||||
dup.id, len(dup.child_ids),
|
||||
)
|
||||
else:
|
||||
dup.unlink()
|
||||
_logger.info(
|
||||
'[recipe-cleanup] Deleted empty duplicate '
|
||||
'ENP-Alum Line sub_process (id %s)',
|
||||
RECIPE_3620_DUPLICATE_TO_DELETE,
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Phase 2 - Backfill kind on all kind=other nodes via resolver
|
||||
# ============================================================
|
||||
from odoo.addons.fusion_plating import (
|
||||
fp_resolve_step_kind,
|
||||
RESOLVER_KIND_TO_ACTIVE_KIND,
|
||||
)
|
||||
Kind = env['fp.step.kind']
|
||||
other_kind = Kind.search([('code', '=', 'other')], limit=1)
|
||||
if not other_kind:
|
||||
_logger.error(
|
||||
'[recipe-cleanup] No "other" kind found; skipping kind '
|
||||
'backfill phase'
|
||||
)
|
||||
else:
|
||||
# Cache code -> kind.id so we don't search per-row.
|
||||
kind_by_code = {k.code: k.id for k in Kind.search([])}
|
||||
affected_nodes = Node.search([
|
||||
('kind_id', '=', other_kind.id),
|
||||
('name', '!=', False),
|
||||
('node_type', 'in', ('operation', 'step', 'sub_process')),
|
||||
])
|
||||
fixed = 0
|
||||
for node in affected_nodes:
|
||||
resolver_code = fp_resolve_step_kind(node.name)
|
||||
if not resolver_code:
|
||||
continue
|
||||
target_code = RESOLVER_KIND_TO_ACTIVE_KIND.get(resolver_code)
|
||||
if not target_code or target_code not in kind_by_code:
|
||||
continue
|
||||
node.with_context(
|
||||
fp_skip_kind_autoclassify=True,
|
||||
).write({'kind_id': kind_by_code[target_code]})
|
||||
fixed += 1
|
||||
_logger.info(
|
||||
'[recipe-cleanup] Phase 2: backfilled kind on %s nodes '
|
||||
'(of %s currently kind=other)',
|
||||
fixed, len(affected_nodes),
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Phase 3 - Delete all per-part clone recipes
|
||||
# ============================================================
|
||||
# Identify by name pattern. The configurator names clones
|
||||
# "BASE_NAME - PART_NUMBER Rev X" with an em-dash separator
|
||||
# (U+2014). No base recipe uses em-dash in its name.
|
||||
#
|
||||
# IMPORTANT: delete one-clone-at-a-time with savepoint per clone.
|
||||
# Batch unlink (clone_recipes.unlink()) tripped a PostgreSQL FK
|
||||
# cascade ordering bug on entech (insert-or-update on parent_id
|
||||
# during the cascade chain). Per-clone unlink with intermediate
|
||||
# cleanup avoids that path entirely and lets one bad clone fail
|
||||
# without rolling back the others.
|
||||
clone_recipes = Node.search([
|
||||
('node_type', '=', 'recipe'),
|
||||
('name', 'ilike', '% - %'),
|
||||
])
|
||||
if not clone_recipes:
|
||||
_logger.info(
|
||||
'[recipe-cleanup] Phase 3: no clone recipes found '
|
||||
'(already deleted on a prior run, or none exist)'
|
||||
)
|
||||
else:
|
||||
_logger.info(
|
||||
'[recipe-cleanup] Phase 3: deleting %s clone recipes one '
|
||||
'at a time (per-clone savepoint)', len(clone_recipes),
|
||||
)
|
||||
deleted = 0
|
||||
failed = []
|
||||
for clone in clone_recipes:
|
||||
cid, cname = clone.id, clone.name
|
||||
cr.execute('SAVEPOINT delete_clone')
|
||||
try:
|
||||
clone.unlink()
|
||||
cr.execute('RELEASE SAVEPOINT delete_clone')
|
||||
deleted += 1
|
||||
except Exception as e:
|
||||
cr.execute('ROLLBACK TO SAVEPOINT delete_clone')
|
||||
failed.append((cid, cname, type(e).__name__, str(e)[:120]))
|
||||
_logger.warning(
|
||||
'[recipe-cleanup] Phase 3: failed to delete '
|
||||
'clone %s ("%s"): %s - continuing',
|
||||
cid, cname, type(e).__name__,
|
||||
)
|
||||
_logger.info(
|
||||
'[recipe-cleanup] Phase 3: deleted %s/%s clones '
|
||||
'(%s failures retained for manual review)',
|
||||
deleted, len(clone_recipes), len(failed),
|
||||
)
|
||||
if failed:
|
||||
for cid, cname, errtype, errmsg in failed:
|
||||
_logger.warning(
|
||||
'[recipe-cleanup] Phase 3 leftover: id=%s name=%r '
|
||||
'err=%s: %s', cid, cname, errtype, errmsg,
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Phase 4 - Recompute area_kind on all fp.job.step rows
|
||||
# ============================================================
|
||||
Step = env['fp.job.step']
|
||||
steps = Step.search([])
|
||||
if steps:
|
||||
steps._compute_area_kind()
|
||||
steps.flush_recordset(['area_kind'])
|
||||
_logger.info(
|
||||
'[recipe-cleanup] Phase 4: recomputed area_kind on %s steps',
|
||||
len(steps),
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Phase 5 - Recompute active_step_id + card_state on in-flight jobs
|
||||
# ============================================================
|
||||
Job = env['fp.job']
|
||||
jobs = Job.search([
|
||||
('state', 'in', ('confirmed', 'in_progress')),
|
||||
])
|
||||
if jobs:
|
||||
jobs._compute_active_step_id()
|
||||
jobs._compute_card_state()
|
||||
jobs.flush_recordset(['active_step_id', 'card_state'])
|
||||
_logger.info(
|
||||
'[recipe-cleanup] Phase 5: recomputed active_step_id + '
|
||||
'card_state on %s in-flight jobs',
|
||||
len(jobs),
|
||||
)
|
||||
@@ -1,91 +0,0 @@
|
||||
# -*- 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),
|
||||
)
|
||||
@@ -1,89 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1
|
||||
#
|
||||
# Phase 1 (Sub 11) - relocate ir.model.data XML IDs from
|
||||
# fusion_plating_bridge_mrp to fusion_plating_jobs for the four
|
||||
# models that moved: fp.work.role, fp.operator.proficiency,
|
||||
# fp.qc.checklist.template (+line), fp.job.consumption.
|
||||
#
|
||||
# Pre-migration so Odoo's normal load pass sees the records under the
|
||||
# new module owner, not as orphans pending deletion.
|
||||
#
|
||||
# Idempotent - `ON CONFLICT DO NOTHING` skips rows already migrated.
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
if not version:
|
||||
return # Fresh install - nothing to migrate
|
||||
|
||||
moves = [
|
||||
# (xmlid pattern, list of model identifiers to move)
|
||||
('model_fp_job_consumption',),
|
||||
# ACL records (csv:id values get prefixed with the owning module)
|
||||
('access_fp_job_consumption_%',),
|
||||
]
|
||||
for (pat,) in moves:
|
||||
cr.execute(
|
||||
"""
|
||||
UPDATE ir_model_data
|
||||
SET module = 'fusion_plating_jobs'
|
||||
WHERE module = 'fusion_plating_bridge_mrp'
|
||||
AND name LIKE %s
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ir_model_data d2
|
||||
WHERE d2.module = 'fusion_plating_jobs'
|
||||
AND d2.name = ir_model_data.name
|
||||
)
|
||||
""",
|
||||
(pat,),
|
||||
)
|
||||
if cr.rowcount:
|
||||
_logger.info(
|
||||
"Sub 11: re-keyed %d ir.model.data rows matching %s -> fusion_plating_jobs",
|
||||
cr.rowcount, pat,
|
||||
)
|
||||
|
||||
# Views, actions, menus that the old module created
|
||||
view_patterns = [
|
||||
'view_fp_job_consumption_%',
|
||||
'action_fp_job_consumption%',
|
||||
'menu_fp_job_consumption%',
|
||||
]
|
||||
for pat in view_patterns:
|
||||
cr.execute(
|
||||
"""
|
||||
UPDATE ir_model_data
|
||||
SET module = 'fusion_plating_jobs'
|
||||
WHERE module = 'fusion_plating_bridge_mrp'
|
||||
AND name LIKE %s
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ir_model_data d2
|
||||
WHERE d2.module = 'fusion_plating_jobs'
|
||||
AND d2.name = ir_model_data.name
|
||||
)
|
||||
""",
|
||||
(pat,),
|
||||
)
|
||||
if cr.rowcount:
|
||||
_logger.info(
|
||||
"Sub 11: re-keyed %d row(s) for %s -> fusion_plating_jobs",
|
||||
cr.rowcount, pat,
|
||||
)
|
||||
|
||||
# Phase 1 swap: fp.job.consumption columns. Drop the legacy
|
||||
# MRP-pointing columns (production_id, workorder_id) from the
|
||||
# already-existing table - there are zero rows referencing MRP, and
|
||||
# the new model declares job_id / step_id instead.
|
||||
cr.execute(
|
||||
"""
|
||||
ALTER TABLE fp_job_consumption
|
||||
DROP COLUMN IF EXISTS production_id,
|
||||
DROP COLUMN IF EXISTS workorder_id
|
||||
"""
|
||||
)
|
||||
_logger.info("Sub 11: dropped MRP columns on fp_job_consumption")
|
||||
Reference in New Issue
Block a user