349 lines
12 KiB
Python
349 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
|
|
import logging
|
|
|
|
from . import controllers
|
|
from . import models
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def post_init_hook(env):
|
|
"""Run on first install / module upgrade. Idempotent.
|
|
|
|
Does several things, each guarded by an "is this already done?"
|
|
check so re-running the hook doesn't clobber state:
|
|
1. Auto-detect a sensible default timezone (original behavior).
|
|
2. Sub 12a — backfill `kind='step_input'` on existing
|
|
fusion.plating.process.node.input rows that pre-date the
|
|
`kind` field.
|
|
3. Sub 12a — seed fp.step.template with starter library entries
|
|
derived from ENP-ALUM-BASIC if the library is currently empty.
|
|
4. Sub 12b — seed 4 starter rack tags if the registry is empty.
|
|
"""
|
|
_seed_default_timezone(env)
|
|
_backfill_node_input_kind(env)
|
|
_seed_step_library_if_empty(env)
|
|
_backfill_contract_review_template(env)
|
|
_seed_rack_tags_if_empty(env)
|
|
_migrate_legacy_uom_columns(env)
|
|
|
|
|
|
def _resolve_kind_id(env, code):
|
|
"""Look up an fp.step.kind id by code. Returns False if not found.
|
|
Cheap helper used during seeding so legacy code paths that referenced
|
|
string codes can keep their semantics."""
|
|
if not code:
|
|
return False
|
|
rec = env['fp.step.kind'].search(
|
|
[('code', '=', code)], limit=1,
|
|
)
|
|
return rec.id or False
|
|
|
|
|
|
def _backfill_contract_review_template(env):
|
|
"""Idempotent — ensure the Contract Review library template exists.
|
|
|
|
`_seed_step_library_if_empty` only fires on a fresh DB; existing DBs
|
|
upgraded from pre-Policy-B versions still have a populated library
|
|
minus the Contract Review entry. This function fills that hole.
|
|
Re-running it is a no-op once the template exists.
|
|
"""
|
|
Tpl = env['fp.step.template']
|
|
if Tpl.search([('default_kind', '=', 'contract_review')], limit=1):
|
|
return # already there
|
|
tpl = Tpl.create({
|
|
'name': 'Contract Review',
|
|
'kind_id': _resolve_kind_id(env, 'contract_review'),
|
|
})
|
|
tpl.action_seed_default_inputs()
|
|
_logger.info(
|
|
"Fusion Plating: backfilled Contract Review library template "
|
|
"(id=%s, %s default inputs).",
|
|
tpl.id, len(tpl.input_template_ids),
|
|
)
|
|
|
|
|
|
def _seed_default_timezone(env):
|
|
from .models.fp_tz import detect_default_tz
|
|
|
|
detected = detect_default_tz(env)
|
|
for company in env['res.company'].sudo().search([]):
|
|
if not company.x_fc_default_tz:
|
|
company.x_fc_default_tz = detected
|
|
_logger.info(
|
|
'Fusion Plating: set default timezone for company %s -> %s',
|
|
company.name, detected,
|
|
)
|
|
|
|
|
|
def _backfill_node_input_kind(env):
|
|
"""Sub 12a — set kind='step_input' on rows that have NULL kind."""
|
|
cr = env.cr
|
|
cr.execute(
|
|
"UPDATE fusion_plating_process_node_input "
|
|
"SET kind = 'step_input' WHERE kind IS NULL"
|
|
)
|
|
if cr.rowcount:
|
|
_logger.info(
|
|
"Fusion Plating: backfilled kind='step_input' on %s "
|
|
"fusion.plating.process.node.input rows", cr.rowcount,
|
|
)
|
|
|
|
|
|
# Mapping of recipe-step name → default_kind. Drives sane-default
|
|
# input seeding on the starter library entries.
|
|
_STARTER_KIND_BY_NAME = {
|
|
# Policy B (2026-04-28) — recipe-side Contract Review step.
|
|
# When an author drops this template into a recipe, fp.job.step.button_*
|
|
# hooks in fusion_plating_jobs detect the kind=='contract_review' and
|
|
# auto-open / gate the QA-005 audit form (fp.contract.review).
|
|
'contract review': 'contract_review',
|
|
'qa-005': 'contract_review',
|
|
'soak clean': 'cleaning',
|
|
'electroclean': 'cleaning',
|
|
'solvent clean': 'cleaning',
|
|
'rinse': 'rinse',
|
|
'primary rinse': 'rinse',
|
|
'secondary rinse': 'rinse',
|
|
'hot rinse': 'rinse',
|
|
'final rinse': 'rinse',
|
|
'etch': 'etch',
|
|
'desmut': 'etch',
|
|
'zincate': 'etch',
|
|
'strip zincate': 'etch',
|
|
'acid dip': 'etch',
|
|
'hcl activation': 'etch',
|
|
'water break test': 'wbf_test',
|
|
'water break free test': 'wbf_test',
|
|
'issue panels': 'mask',
|
|
'masking': 'mask',
|
|
'mask': 'mask',
|
|
'racking': 'racking',
|
|
'rack': 'racking',
|
|
'e-nickel plate': 'plate',
|
|
'e-nickel plating': 'plate',
|
|
'electroless nickel plate': 'plate',
|
|
'electroless nickel plating': 'plate',
|
|
'enp': 'plate',
|
|
'plate': 'plate',
|
|
'plating': 'plate',
|
|
'drying': 'dry',
|
|
'dry': 'dry',
|
|
'bake': 'bake',
|
|
'oven baking': 'bake',
|
|
'oven bake': 'bake',
|
|
'baking': 'bake',
|
|
'hydrogen embrittlement bake': 'bake',
|
|
'he bake': 'bake',
|
|
'de-rack': 'derack',
|
|
'de-racking': 'derack',
|
|
'deracking': 'derack',
|
|
'derack': 'derack',
|
|
'demask': 'demask',
|
|
'de-mask': 'demask',
|
|
'de-masking': 'demask',
|
|
'demasking': 'demask',
|
|
'inspection': 'inspect',
|
|
'incoming inspection': 'inspect',
|
|
'post-plate inspection': 'inspect',
|
|
'post plate inspection': 'inspect',
|
|
'visual inspection': 'inspect',
|
|
'porosity test': 'inspect',
|
|
'adhesion test': 'inspect',
|
|
'final inspection': 'final_inspect',
|
|
'final inspection / packaging': 'final_inspect',
|
|
'shipping': 'ship',
|
|
'pack': 'ship',
|
|
'packaging': 'ship',
|
|
# Gating steps (Steelhead-style "Ready for X" intermediate states).
|
|
'ready for incoming inspection': 'gating',
|
|
'ready for plating': 'gating',
|
|
'ready for racking': 'gating',
|
|
'ready for de-masking': 'gating',
|
|
'ready for demasking': 'gating',
|
|
'ready for masking': 'gating',
|
|
'ready for bake': 'gating',
|
|
'ready for deracking': 'gating',
|
|
'ready for de-racking': 'gating',
|
|
'ready for post plate inspection': 'gating',
|
|
'ready for post-plate inspection': 'gating',
|
|
'ready for final inspection': 'gating',
|
|
'ready for shipping': 'gating',
|
|
}
|
|
|
|
|
|
def fp_resolve_step_kind(name):
|
|
"""Resolve a step name to a default_kind, tolerant of whitespace and
|
|
case. Used by both the seeder and the migration backfill so we don't
|
|
have two slightly-different lookup paths.
|
|
|
|
Returns the kind str or None when no match.
|
|
"""
|
|
if not name:
|
|
return None
|
|
key = name.strip().lower()
|
|
if key in _STARTER_KIND_BY_NAME:
|
|
return _STARTER_KIND_BY_NAME[key]
|
|
# Gating "Ready for / Ready For" prefix — anything starting with that
|
|
# is a gating node regardless of the destination step name.
|
|
if key.startswith('ready for ') or key.startswith('ready '):
|
|
return 'gating'
|
|
return None
|
|
|
|
|
|
def _seed_step_library_if_empty(env):
|
|
"""Sub 12a — seed fp.step.template starter library.
|
|
|
|
Source priority:
|
|
1. ENP-ALUM-BASIC recipe's child nodes (best — reuses the
|
|
author-curated step set).
|
|
2. Hard-coded minimal list (fallback for fresh DBs).
|
|
"""
|
|
Tpl = env['fp.step.template']
|
|
if Tpl.search_count([]):
|
|
_logger.info(
|
|
'Fusion Plating: step library already populated, skip seed',
|
|
)
|
|
return
|
|
|
|
Node = env['fusion.plating.process.node']
|
|
src = Node.search([
|
|
('node_type', '=', 'recipe'),
|
|
'|', ('code', '=', 'ENP-ALUM-BASIC'),
|
|
('name', 'ilike', 'ENP-ALUM-BASIC'),
|
|
], limit=1)
|
|
|
|
if not src:
|
|
_seed_minimal_library(env)
|
|
return
|
|
|
|
seen = set()
|
|
for child in src.child_ids:
|
|
if child.node_type == 'step':
|
|
_create_template_from_node(env, child, seen)
|
|
else:
|
|
for grandchild in child.child_ids:
|
|
_create_template_from_node(env, grandchild, seen)
|
|
|
|
_logger.info(
|
|
"Fusion Plating: seeded step library with %s entries from %s",
|
|
len(seen), src.name,
|
|
)
|
|
|
|
|
|
def _create_template_from_node(env, node, seen):
|
|
if not node.name or node.name.lower() in seen:
|
|
return
|
|
seen.add(node.name.lower())
|
|
|
|
kind = fp_resolve_step_kind(node.name)
|
|
vals = {
|
|
'name': node.name,
|
|
'description': node.description or False,
|
|
'icon': node.icon or 'fa-cog',
|
|
'process_type_id': node.process_type_id.id,
|
|
'requires_signoff': node.requires_signoff,
|
|
'requires_predecessor_done': node.requires_predecessor_done,
|
|
'kind_id': _resolve_kind_id(env, kind),
|
|
}
|
|
# Snapshot tank_ids if the node has them (added by Sub 12a;
|
|
# existing nodes may not).
|
|
if 'tank_ids' in node._fields and node.tank_ids:
|
|
vals['tank_ids'] = [(6, 0, node.tank_ids.ids)]
|
|
# Snapshot any time/temp targets the node may already carry.
|
|
for f in ('time_min_target', 'time_max_target', 'time_unit',
|
|
'temp_min_target', 'temp_max_target', 'temp_unit'):
|
|
if f in node._fields:
|
|
vals[f] = node[f] or vals.get(f)
|
|
|
|
tpl = env['fp.step.template'].create(vals)
|
|
if kind:
|
|
tpl.action_seed_default_inputs()
|
|
|
|
|
|
def _seed_minimal_library(env):
|
|
"""Hard-coded minimal seed when ENP-ALUM-BASIC isn't on the target DB."""
|
|
Tpl = env['fp.step.template']
|
|
minimal = [
|
|
('Contract Review', 'contract_review'),
|
|
('Soak Clean', 'cleaning'),
|
|
('Electroclean', 'cleaning'),
|
|
('Rinse', 'rinse'),
|
|
('Etch', 'etch'),
|
|
('Desmut', 'etch'),
|
|
('Zincate', 'etch'),
|
|
('Acid Dip', 'etch'),
|
|
('Water Break Test', 'wbf_test'),
|
|
('Racking', 'racking'),
|
|
('De-Racking', 'derack'),
|
|
('E-Nickel Plate', 'plate'),
|
|
('Drying', 'dry'),
|
|
('Inspection', 'inspect'),
|
|
('Final Inspection', 'final_inspect'),
|
|
('Shipping', 'ship'),
|
|
]
|
|
for name, kind in minimal:
|
|
tpl = Tpl.create({
|
|
'name': name,
|
|
'kind_id': _resolve_kind_id(env, kind),
|
|
})
|
|
tpl.action_seed_default_inputs()
|
|
_logger.info(
|
|
'Fusion Plating: seeded minimal step library (%s entries)',
|
|
len(minimal),
|
|
)
|
|
|
|
|
|
def _migrate_legacy_uom_columns(env):
|
|
"""Translate every free-text UoM column in the plating suite into the
|
|
new curated Selection keys.
|
|
|
|
Runs unconditionally on every fusion_plating upgrade so the day a
|
|
downstream module's migration converts a Char to Selection, the data
|
|
follows. Each call is a no-op when:
|
|
* the column already holds selection keys (identity mapping)
|
|
* the table doesn't exist (module not installed on this DB)
|
|
"""
|
|
from .models._fp_uom_selection import fp_migrate_uom_column
|
|
|
|
targets = [
|
|
# core
|
|
('fusion_plating_bath_parameter', 'uom', 'bath parameter'),
|
|
('fusion_plating_process_node_input', 'uom', 'process node input'),
|
|
('fusion_plating_process_node_input', 'target_unit', 'process node target'),
|
|
('fp_step_template_input', 'target_unit', 'step template input target'),
|
|
# compliance
|
|
('fusion_plating_discharge_limit', 'uom', 'discharge limit'),
|
|
('fusion_plating_discharge_sample_line', 'uom', 'discharge sample line'),
|
|
('fusion_plating_waste_manifest', 'uom', 'waste manifest'),
|
|
('fusion_plating_waste_stream', 'generation_uom', 'waste stream'),
|
|
('fusion_plating_spill_register', 'uom', 'spill register'),
|
|
# safety
|
|
('fusion_plating_chemical', 'container_uom', 'chemical container'),
|
|
('fusion_plating_exposure_monitoring', 'uom', 'exposure monitoring'),
|
|
]
|
|
for table, column, label in targets:
|
|
fp_migrate_uom_column(env, table, column, label)
|
|
|
|
|
|
def _seed_rack_tags_if_empty(env):
|
|
"""Sub 12b — seed 4 starter rack tags."""
|
|
Tag = env['fp.rack.tag']
|
|
if Tag.search_count([]):
|
|
return
|
|
starters = [
|
|
('Rush', 1),
|
|
('Hold for QC', 3),
|
|
('Damaged', 9),
|
|
('Customer Sample', 5),
|
|
]
|
|
for name, color in starters:
|
|
Tag.create({'name': name, 'color': color})
|
|
_logger.info(
|
|
'Fusion Plating: seeded %s starter rack tags', len(starters),
|
|
)
|