Implements 2026-04-29-step-library-audit-design.md. Bumps fusion_plating to 19.0.18.7.0, fusion_plating_jobs to 19.0.8.12.0, fusion_plating_reports to 19.0.10.2.0. LIBRARY EXPANSION - 8 new Step Kinds: Receiving, Electroclean, Strike, Salt Spray, Adhesion Test, Hardness Test, Packaging, Tank Replenishment - 4 new input types: photo, multi_point_thickness, bath_chemistry_panel, ph - DEFAULT_INPUTS_BY_KIND rewritten to seed audit-grade prompts on every kind (bath IDs, photos, multi-point thickness, signatures, etc.) - + Common Audit Fields one-click button on the library template form - Default Operator Instructions relabel + alert callout PER-RECIPE CONFIGURABILITY - collect (Boolean) per recipe-step input prompt — opt out without delete - collect_measurements (Boolean) master switch on recipe step — when off, wizard skips entirely - template_input_id (Many2one) traceability link from recipe to library - Recipe-step backend form view exposes the new fields with handle drag, toggle, target range, and library-source column RUNTIME WIRING - Step input wizard filters node.input_ids to step_input AND collect=True; short-circuits on collect_measurements=False - New input types: photo (image widget + ir.attachment), multi-point thickness (5 readings + auto avg, skips empty cells), bath chemistry panel (pH/conc/temp/bath bundle), pH (0-14 numeric) - Composite values JSON-serialized into value_text; photo via attachment CoC REPORT - Filters captured prompts to collect=True only - Renders new input types with appropriate format MIGRATION (post-migrate.py for 19.0.18.7.0) - Backfills collect=True on recipe-step inputs - Backfills collect_measurements=True on recipe steps - Re-runs action_seed_default_inputs on every existing template (idempotent, preserves user edits) - Backfills template_input_id by name-matching against source library template (handles JSONB vs varchar name columns) SEED DATA - 8 example templates (one per new kind) in fp_step_template_data.xml with noupdate=1 BATTLE TEST - bt_step_library_audit.py: 29 assertions all PASS on entech OWL EDITOR EXTENSION DEFERRED - The simple recipe editor's per-step Instructions/Measurements expansions were not implemented in this pass; users configure via the backend recipe-step form. Track follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
4.5 KiB
Python
130 lines
4.5 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.
|
|
|
|
from . import controllers
|
|
from . import models
|
|
from . import wizard
|
|
|
|
|
|
def _backfill_currency(env):
|
|
"""Fill missing currency_id on existing money-holding records.
|
|
|
|
Older demo data and manually-created rows were persisted before the
|
|
`required=True` was added, so some records sit with currency_id=NULL
|
|
and Monetary fields render without a $ symbol. This runs on module
|
|
install/upgrade and pins them to the company's currency.
|
|
"""
|
|
company_currency = env.company.currency_id.id
|
|
if not company_currency:
|
|
return
|
|
for model_name in (
|
|
'fp.pricing.rule',
|
|
'fp.treatment',
|
|
'fp.customer.price.list',
|
|
'fp.quote.configurator',
|
|
):
|
|
Model = env.get(model_name)
|
|
if Model is None:
|
|
continue
|
|
Model.search([('currency_id', '=', False)]).write(
|
|
{'currency_id': company_currency}
|
|
)
|
|
|
|
|
|
def _backfill_cloned_process_names(env):
|
|
"""Append " — <part_number> Rev <revision>" to every existing part-
|
|
cloned process ROOT whose name doesn't already carry the suffix.
|
|
|
|
Feedback on 2026-04-23: the Process tab on the part form was
|
|
showing a bare template name ("General Processing"), so users
|
|
couldn't tell at a glance that the clone belonged to THIS part.
|
|
The clone logic now adds the suffix automatically; this backfill
|
|
brings older clones up to the same format without forcing
|
|
users to re-compose (which would wipe their edits).
|
|
|
|
Idempotent: checks for a literal " — " separator before rewriting.
|
|
"""
|
|
Node = env['fusion.plating.process.node']
|
|
roots = Node.search([
|
|
('node_type', '=', 'recipe'),
|
|
('part_catalog_id', '!=', False),
|
|
('parent_id', '=', False),
|
|
])
|
|
renamed = 0
|
|
for root in roots:
|
|
part = root.part_catalog_id
|
|
if not part:
|
|
continue
|
|
if ' — ' in (root.name or ''):
|
|
continue # Already has a suffix — leave alone.
|
|
suffix_bits = []
|
|
if part.part_number:
|
|
suffix_bits.append(part.part_number)
|
|
if part.revision:
|
|
# `revision` sometimes already carries a "Rev " prefix
|
|
# (e.g. "Rev 2") — don't double up.
|
|
rev = part.revision.strip()
|
|
if not rev.lower().startswith('rev'):
|
|
rev = 'Rev %s' % rev
|
|
suffix_bits.append(rev)
|
|
if not suffix_bits:
|
|
continue
|
|
root.name = '%s — %s' % (root.name or '', ' '.join(suffix_bits))
|
|
renamed += 1
|
|
|
|
|
|
def _backfill_part_material_id(env):
|
|
"""Pin existing parts AND quote configurators to a row in the
|
|
shared material library.
|
|
|
|
Pre-Sub-12d, both models only had a `substrate_material` Selection.
|
|
This sets `material_id` on every record that doesn't yet have one,
|
|
matching by substrate_material → seed material XML id. Idempotent.
|
|
"""
|
|
Part = env['fp.part.catalog']
|
|
Material = env['fp.part.material']
|
|
if Part is None or Material is None:
|
|
return
|
|
# Map legacy Selection key → seed XML id (the generic per-category entry).
|
|
xmlid_by_key = {
|
|
'aluminium': 'fusion_plating_configurator.fp_material_aluminium',
|
|
'steel': 'fusion_plating_configurator.fp_material_steel',
|
|
'stainless': 'fusion_plating_configurator.fp_material_stainless',
|
|
'copper': 'fusion_plating_configurator.fp_material_copper',
|
|
'titanium': 'fusion_plating_configurator.fp_material_titanium',
|
|
'other': 'fusion_plating_configurator.fp_material_other',
|
|
}
|
|
cache = {}
|
|
for key, xmlid in xmlid_by_key.items():
|
|
rec = env.ref(xmlid, raise_if_not_found=False)
|
|
if rec:
|
|
cache[key] = rec.id
|
|
if not cache:
|
|
return
|
|
# Parts
|
|
for part in Part.search([('material_id', '=', False)]):
|
|
mid = cache.get(part.substrate_material)
|
|
if mid:
|
|
part.material_id = mid
|
|
# Quote configurators (same Selection key → same library)
|
|
Quote = env['fp.quote.configurator']
|
|
if Quote is not None:
|
|
for q in Quote.search([('material_id', '=', False)]):
|
|
mid = cache.get(q.substrate_material)
|
|
if mid:
|
|
q.material_id = mid
|
|
|
|
|
|
def post_init_hook(env):
|
|
_backfill_currency(env)
|
|
_backfill_cloned_process_names(env)
|
|
_backfill_part_material_id(env)
|
|
|
|
|
|
def post_upgrade_hook(env):
|
|
_backfill_currency(env)
|
|
_backfill_cloned_process_names(env)
|
|
_backfill_part_material_id(env)
|