Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
618 lines
23 KiB
Python
618 lines
23 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
|
|
import re
|
|
|
|
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.
|
|
5. Phase H - create a pending fp.migration.preview if any user
|
|
still holds an old plating-role group + notify Owners.
|
|
"""
|
|
_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)
|
|
_seed_starter_recipes_once(env)
|
|
_fp_post_init_role_migration(env)
|
|
_fp_apply_office_user_menu_visibility(env)
|
|
|
|
|
|
# Top-level app menus that technicians should NOT see. Each entry is an
|
|
# xmlid; env.ref(..., raise_if_not_found=False) silently skips menus
|
|
# from uninstalled modules so this is safe across configurations.
|
|
# Kept visible to technicians (NOT in this list): Discuss, To-do,
|
|
# Plating, AI, Maintenance, Time Off. Settings/Apps/Tests are admin-
|
|
# restricted upstream - also not in this list.
|
|
# See security/fp_menu_visibility.xml for the design rationale.
|
|
MENU_HIDE_FROM_TECHNICIANS = [
|
|
'calendar.mail_menu_calendar',
|
|
'contacts.menu_contacts',
|
|
'crm.crm_menu_root',
|
|
'sale.sale_menu_root',
|
|
'spreadsheet_dashboard.spreadsheet_dashboard_menu_root',
|
|
'fusion_ringcentral.menu_rc_root',
|
|
'fusion_faxes.menu_fusion_faxes_root',
|
|
'fusion_tasks.menu_field_service_root',
|
|
'fusion_clock.menu_fusion_clock_root',
|
|
'account.menu_finance',
|
|
'accountant.menu_accounting',
|
|
'project.menu_main_pm',
|
|
'hr_timesheet.timesheet_menu_root',
|
|
'planning.planning_menu_root',
|
|
'fusion_shipping.menu_fusion_shipping_root',
|
|
'website.menu_website_configuration',
|
|
'purchase.menu_purchase_root',
|
|
'stock.menu_stock_root',
|
|
'sign.menu_document',
|
|
'hr.menu_hr_root',
|
|
'hr_work_entry_enterprise.menu_hr_payroll_root',
|
|
'hr_attendance.menu_hr_attendance_root',
|
|
'hr_recruitment.menu_hr_recruitment_root',
|
|
'hr_expense.menu_hr_expense_root',
|
|
'iot.iot_menu_root',
|
|
'utm.menu_link_tracker_root',
|
|
'base.menu_management',
|
|
]
|
|
|
|
|
|
def _fp_apply_office_user_menu_visibility(env):
|
|
"""Set group_ids = [group_fp_office_user] on every menu in
|
|
MENU_HIDE_FROM_TECHNICIANS that exists in this DB.
|
|
|
|
Field is `group_ids` on ir.ui.menu in Odoo 19 (was `groups_id` in
|
|
earlier versions - Odoo 18 renamed it). Same naming-rename pattern
|
|
as res.users (CLAUDE.md Critical Rule 13c).
|
|
|
|
Idempotent: if a menu already has only the office_user group, no
|
|
change is made. If it has additional groups (e.g. a previous custom
|
|
restriction), they're REPLACED - the design accepts this trade-off
|
|
because office_user is implied by every fp role above Technician,
|
|
so non-fp users keep their access on entech.
|
|
|
|
Cross-module xmlids: env.ref(..., raise_if_not_found=False) returns
|
|
None for menus from uninstalled modules, which we silently skip.
|
|
"""
|
|
office = env.ref(
|
|
'fusion_plating.group_fp_office_user', raise_if_not_found=False,
|
|
)
|
|
if not office:
|
|
_logger.warning(
|
|
'[menu-visibility] group_fp_office_user not found; skipping'
|
|
)
|
|
return
|
|
touched = 0
|
|
for xmlid in MENU_HIDE_FROM_TECHNICIANS:
|
|
menu = env.ref(xmlid, raise_if_not_found=False)
|
|
if not menu:
|
|
continue
|
|
current_ids = set(menu.group_ids.ids)
|
|
if current_ids == {office.id}:
|
|
continue # already locked-down, nothing to do
|
|
menu.sudo().group_ids = [(6, 0, [office.id])]
|
|
touched += 1
|
|
_logger.info(
|
|
'[menu-visibility] restricted %s menu(s) to group_fp_office_user',
|
|
touched,
|
|
)
|
|
|
|
|
|
def _fp_post_init_role_migration(env):
|
|
"""Idempotent: creates a fp.migration.preview if none is pending or applied.
|
|
|
|
Called automatically on `-u fusion_plating`. The preview enters 'pending'
|
|
state and schedules a mail.activity on every Owner. Owner must explicitly
|
|
click 'Approve & Run' to actually apply the migration.
|
|
"""
|
|
Preview = env['fp.migration.preview']
|
|
if Preview.search_count([('state', '=', 'pending')]):
|
|
return
|
|
if Preview.search_count([('state', '=', 'approved')]):
|
|
# Already migrated previously; only re-fire if any unmigrated user remains
|
|
# An unmigrated user is one who still holds an OLD plating group directly
|
|
# AND does NOT hold any NEW role group. The compute on res.users.x_fc_plating_role
|
|
# returns 'no' for users without any new group regardless of their old groups.
|
|
# Heuristic: if any active user still holds an old group, re-fire.
|
|
from .models.fp_role_constants import _FP_OLD_GROUP_XMLIDS
|
|
any_unmigrated = False
|
|
for xmlid in _FP_OLD_GROUP_XMLIDS:
|
|
old_grp = env.ref(xmlid, raise_if_not_found=False)
|
|
if not old_grp:
|
|
continue
|
|
if old_grp.users.filtered(lambda u: u.active and not u.share):
|
|
# Found at least one user still on an old group → re-fire
|
|
any_unmigrated = True
|
|
break
|
|
if not any_unmigrated:
|
|
return # All users migrated; nothing to do
|
|
preview = Preview.create({})
|
|
preview._fp_build_lines()
|
|
preview._fp_notify_owners()
|
|
|
|
|
|
def _seed_starter_recipes_once(env):
|
|
"""Load starter recipe XML files on FIRST install only.
|
|
|
|
Before 19.0.20.5.0 the recipe XML files (ENP-STEEL-BASIC, ENP-SP,
|
|
ENP-ALUM-BASIC, etc.) lived in the manifest's ``data`` list. With
|
|
``noupdate="1"`` we expected user edits / deletions to survive
|
|
module upgrades - but Odoo only treats noupdate=1 as "don't update
|
|
existing records". If a record's ir.model.data row is deleted via
|
|
unlink, Odoo on the next ``-u`` sees the xmlid as missing and
|
|
RE-CREATES the record from XML. Bug reported 2026-05-20: every
|
|
time the user deleted a substep from a starter recipe, the next
|
|
upgrade brought it back.
|
|
|
|
Fix: pull those files out of the manifest's data list, load them
|
|
here via convert_file ONCE per xmlid. Each file gets a sentinel
|
|
check (does the root recipe's xmlid exist in ir.model.data?); if
|
|
yes, skip. The hook is itself idempotent so it's safe to run on
|
|
every upgrade as well - but the sentinel ensures recipe content
|
|
is only seeded the very first time.
|
|
"""
|
|
from odoo.tools import convert
|
|
Module = env['ir.module.module']
|
|
mod = Module.search([('name', '=', 'fusion_plating')], limit=1)
|
|
if not mod:
|
|
return
|
|
|
|
# (xmlid_to_check, data_file_path) pairs.
|
|
# If the xmlid already exists in ir.model.data, the file is skipped.
|
|
sentinels = [
|
|
('fusion_plating.recipe_enp_alum_basic',
|
|
'data/fp_recipe_enp_alum_basic.xml'),
|
|
('fusion_plating.recipe_enp_steel_basic',
|
|
'data/fp_recipe_enp_steel_basic.xml'),
|
|
('fusion_plating.recipe_enp_sp',
|
|
'data/fp_recipe_enp_sp.xml'),
|
|
('fusion_plating.recipe_general_processing',
|
|
'data/fp_recipe_general_processing.xml'),
|
|
('fusion_plating.recipe_anodize',
|
|
'data/fp_recipe_anodize.xml'),
|
|
('fusion_plating.recipe_chem_conversion',
|
|
'data/fp_recipe_chem_conversion.xml'),
|
|
]
|
|
IMD = env['ir.model.data']
|
|
for xmlid, filepath in sentinels:
|
|
module_name, name = xmlid.split('.', 1)
|
|
if IMD.search_count([('module', '=', module_name), ('name', '=', name)]):
|
|
# Recipe already in DB (either from a previous install, or
|
|
# already loaded by an earlier hook run). Don't touch - user
|
|
# may have made edits.
|
|
continue
|
|
# File not yet loaded for this DB. Run it once.
|
|
try:
|
|
with open_module_data_file(filepath) as fh:
|
|
convert.convert_file(
|
|
env, module_name, filepath, idref={}, mode='init',
|
|
noupdate=True,
|
|
)
|
|
_logger.info('Seeded starter recipe %s', xmlid)
|
|
except FileNotFoundError:
|
|
_logger.warning('Starter recipe file %s not found, skipping',
|
|
filepath)
|
|
except Exception as exc:
|
|
_logger.warning('Could not seed %s: %s', xmlid, exc)
|
|
|
|
|
|
def open_module_data_file(relpath):
|
|
"""Open a file relative to the fusion_plating module root."""
|
|
import os
|
|
here = os.path.dirname(__file__)
|
|
return open(os.path.join(here, relpath), 'rb')
|
|
|
|
|
|
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',
|
|
# 2026-05-24 - Recipe cleanup additions (spec
|
|
# 2026-05-24-recipe-cleanup-design.md). Covers names the existing
|
|
# resolver didn't know that turned up in the entech recipes audit.
|
|
# Blasting variants
|
|
'blasting': 'blast',
|
|
'bead blast': 'blast',
|
|
'bead blasting': 'blast',
|
|
'media blast': 'blast',
|
|
'media blasting': 'blast',
|
|
# Inspection variants
|
|
'adhesion test coupon': 'inspect',
|
|
'adhesion testing': 'inspect',
|
|
'corrosion testing': 'inspect',
|
|
'lab testing': 'inspect',
|
|
'check sulfamate nickel area': 'inspect',
|
|
'pre-measurements': 'inspect',
|
|
'pre measurements': 'inspect',
|
|
'hot water porosity': 'inspect',
|
|
# Strip / chemical conversion / plugging (wet line)
|
|
'strip process': 'wet_process',
|
|
'strip process - al': 'wet_process',
|
|
'nickel strip - aluminum line': 'wet_process',
|
|
'chemical conversion': 'wet_process',
|
|
'trivalent chromate conversion': 'wet_process',
|
|
'plug the threaded holes': 'mask',
|
|
# Misc wet-line variants seen on entech recipes
|
|
'air dry': 'dry',
|
|
'desmut': 'etch',
|
|
'soak clean': 'cleaning',
|
|
'cleaner': 'cleaning',
|
|
'nickel strike': 'plate',
|
|
'nickel strip': 'plate',
|
|
}
|
|
|
|
|
|
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.
|
|
|
|
Handles parenthetical suffixes like "(Standard)", "(If Required)",
|
|
"(A-14 / A)" by stripping them and re-trying the lookup.
|
|
|
|
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]
|
|
# Parenthetical strip - "Masking (If Required)" → "masking",
|
|
# "Incoming Inspection (Standard)" → "incoming inspection",
|
|
# "Trivalent Chromate Conversion (A-14 / A)" → "trivalent chromate conversion".
|
|
bare = re.sub(r'\s*\([^)]*\)\s*', ' ', key).strip()
|
|
if bare and bare != key and bare in _STARTER_KIND_BY_NAME:
|
|
return _STARTER_KIND_BY_NAME[bare]
|
|
# 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
|
|
|
|
|
|
# Translates resolver kind output to the active fp.step.kind.code values.
|
|
# The resolver still returns the OLD vocabulary (cleaning, electroclean,
|
|
# etch, rinse, strike, dry, wbf_test) which were deactivated in
|
|
# 19.0.20.6.0 - those roll up to the active wet_process kind. Other
|
|
# codes pass through 1:1. Used by the auto-classify hook on
|
|
# fusion.plating.process.node + the recipe-cleanup migration
|
|
# (fusion_plating_jobs 19.0.10.26.0).
|
|
RESOLVER_KIND_TO_ACTIVE_KIND = {
|
|
# Wet-line kinds → wet_process (active rollup)
|
|
'cleaning': 'wet_process',
|
|
'electroclean': 'wet_process',
|
|
'etch': 'wet_process',
|
|
'rinse': 'wet_process',
|
|
'strike': 'wet_process',
|
|
'dry': 'wet_process',
|
|
'wbf_test': 'wet_process',
|
|
'wet_process': 'wet_process', # the alias added in 19.0.21.3.0
|
|
# for "Strip Process - AL", "Chemical
|
|
# Conversion", "Trivalent Chromate
|
|
# Conversion" maps DIRECTLY to
|
|
# 'wet_process' - this passthrough
|
|
# entry lets those land correctly.
|
|
# 1:1 mappings (kind exists and is active)
|
|
'contract_review': 'contract_review',
|
|
'mask': 'mask',
|
|
'racking': 'racking',
|
|
'plate': 'plate',
|
|
'bake': 'bake',
|
|
'derack': 'derack',
|
|
'demask': 'demask',
|
|
'inspect': 'inspect',
|
|
'final_inspect': 'final_inspect',
|
|
'ship': 'ship',
|
|
'gating': 'gating',
|
|
'blast': 'blast',
|
|
}
|
|
|
|
|
|
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),
|
|
)
|