feat(step-library): full plating workflow coverage + per-recipe configurability + audit
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>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.8.11.0',
|
||||
'version': '19.0.8.12.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
|
||||
@@ -31,17 +31,21 @@ from odoo.addons.fusion_plating.models._fp_uom_selection import FP_UOM_SELECTION
|
||||
# Same selection list as fp.step.template.input.input_type so authored
|
||||
# rows + ad-hoc rows pick from the same vocabulary.
|
||||
_FP_INPUT_TYPE_SELECTION = [
|
||||
('text', 'Text'),
|
||||
('number', 'Number'),
|
||||
('boolean', 'Yes/No'),
|
||||
('selection', 'Selection'),
|
||||
('date', 'Date / Time'),
|
||||
('signature', 'Signature'),
|
||||
('time_hms', 'Time (HH:MM:SS)'),
|
||||
('time_seconds', 'Time (seconds)'),
|
||||
('temperature', 'Temperature'),
|
||||
('thickness', 'Thickness'),
|
||||
('pass_fail', 'Pass / Fail'),
|
||||
('text', 'Text'),
|
||||
('number', 'Number'),
|
||||
('boolean', 'Yes/No'),
|
||||
('selection', 'Selection'),
|
||||
('date', 'Date / Time'),
|
||||
('signature', 'Signature'),
|
||||
('time_hms', 'Time (HH:MM:SS)'),
|
||||
('time_seconds', 'Time (seconds)'),
|
||||
('temperature', 'Temperature'),
|
||||
('thickness', 'Thickness'),
|
||||
('pass_fail', 'Pass / Fail'),
|
||||
('photo', 'Photo'),
|
||||
('multi_point_thickness', 'Multi-Point Thickness (avg)'),
|
||||
('bath_chemistry_panel', 'Bath Chemistry Panel'),
|
||||
('ph', 'pH'),
|
||||
]
|
||||
|
||||
|
||||
@@ -72,11 +76,18 @@ class FpJobStepInputWizard(models.TransientModel):
|
||||
return defaults
|
||||
defaults['step_id'] = step.id
|
||||
node = step.recipe_node_id
|
||||
# Sub 12d — master switch — when off, return no input rows.
|
||||
if hasattr(node, 'collect_measurements') and not node.collect_measurements:
|
||||
defaults['line_ids'] = []
|
||||
return defaults
|
||||
# Filter to step_input prompts only — transition inputs go on the
|
||||
# Move wizard, not here.
|
||||
# Move wizard, not here. Also filter to collect=True (per-recipe
|
||||
# opt-out, default True).
|
||||
inputs = node.input_ids
|
||||
if 'kind' in inputs._fields:
|
||||
inputs = inputs.filtered(lambda i: i.kind == 'step_input')
|
||||
if 'collect' in inputs._fields:
|
||||
inputs = inputs.filtered(lambda i: i.collect)
|
||||
defaults['line_ids'] = [(0, 0, {
|
||||
'node_input_id': inp.id,
|
||||
'name': inp.name,
|
||||
@@ -119,6 +130,7 @@ class FpJobStepInputWizard(models.TransientModel):
|
||||
'moved_by_user_id': self.env.user.id,
|
||||
})
|
||||
ValueModel = self.env['fp.job.step.move.input.value']
|
||||
Attachment = self.env['ir.attachment']
|
||||
captured = 0
|
||||
for line in self.line_ids:
|
||||
if not line._has_value():
|
||||
@@ -131,6 +143,33 @@ class FpJobStepInputWizard(models.TransientModel):
|
||||
'value_boolean': line.value_boolean,
|
||||
'value_date': line.value_date or False,
|
||||
}
|
||||
# Sub 12d — composite + photo input types serialise differently.
|
||||
if line.is_photo_type and line.photo_value:
|
||||
att = Attachment.create({
|
||||
'name': line.photo_filename or 'photo.jpg',
|
||||
'datas': line.photo_value,
|
||||
'res_model': 'fp.job.step.move',
|
||||
'res_id': move.id,
|
||||
})
|
||||
vals['value_attachment_id'] = att.id
|
||||
elif line.is_multi_point_type:
|
||||
import json
|
||||
pts = [line.point_1, line.point_2, line.point_3,
|
||||
line.point_4, line.point_5]
|
||||
non_empty = [p for p in pts if p]
|
||||
avg = sum(non_empty) / len(non_empty) if non_empty else 0.0
|
||||
vals['value_text'] = json.dumps({
|
||||
'readings': pts, 'avg': avg,
|
||||
})
|
||||
vals['value_number'] = avg
|
||||
elif line.is_panel_type:
|
||||
import json
|
||||
vals['value_text'] = json.dumps({
|
||||
'ph': line.panel_ph,
|
||||
'concentration': line.panel_concentration,
|
||||
'temperature': line.panel_temperature,
|
||||
'bath_id': line.panel_bath_id or '',
|
||||
})
|
||||
# For ad-hoc rows (no node_input_id), preserve the operator's
|
||||
# typed prompt label in value_text so the chronological CoC
|
||||
# report still shows what was measured. Format: "Prompt: value"
|
||||
@@ -203,6 +242,35 @@ class FpJobStepInputWizardLine(models.TransientModel):
|
||||
value_boolean = fields.Boolean(string='Yes/No')
|
||||
value_date = fields.Datetime(string='Date / Time')
|
||||
|
||||
# Sub 12d — composite + photo input types
|
||||
photo_value = fields.Binary(string='Photo', attachment=True)
|
||||
photo_filename = fields.Char(string='Photo Filename')
|
||||
point_1 = fields.Float(string='R1')
|
||||
point_2 = fields.Float(string='R2')
|
||||
point_3 = fields.Float(string='R3')
|
||||
point_4 = fields.Float(string='R4')
|
||||
point_5 = fields.Float(string='R5')
|
||||
point_avg = fields.Float(
|
||||
string='Average',
|
||||
compute='_compute_point_avg',
|
||||
store=False,
|
||||
)
|
||||
panel_ph = fields.Float(string='Panel pH')
|
||||
panel_concentration = fields.Float(string='Panel Concentration')
|
||||
panel_temperature = fields.Float(string='Panel Temperature')
|
||||
panel_bath_id = fields.Char(string='Panel Bath ID')
|
||||
|
||||
@api.depends('point_1', 'point_2', 'point_3', 'point_4', 'point_5')
|
||||
def _compute_point_avg(self):
|
||||
for rec in self:
|
||||
pts = [
|
||||
p for p in (
|
||||
rec.point_1, rec.point_2, rec.point_3,
|
||||
rec.point_4, rec.point_5,
|
||||
) if p
|
||||
]
|
||||
rec.point_avg = sum(pts) / len(pts) if pts else 0.0
|
||||
|
||||
is_authored = fields.Boolean(
|
||||
compute='_compute_is_authored',
|
||||
help='True when this row originated from an authored recipe input. '
|
||||
@@ -233,20 +301,39 @@ class FpJobStepInputWizardLine(models.TransientModel):
|
||||
compute='_compute_type_flags',
|
||||
)
|
||||
|
||||
is_photo_type = fields.Boolean(compute='_compute_type_flags')
|
||||
is_multi_point_type = fields.Boolean(compute='_compute_type_flags')
|
||||
is_panel_type = fields.Boolean(compute='_compute_type_flags')
|
||||
|
||||
@api.depends('input_type')
|
||||
def _compute_type_flags(self):
|
||||
numeric_types = {
|
||||
'number', 'temperature', 'thickness',
|
||||
'time_seconds',
|
||||
'time_seconds', 'ph',
|
||||
}
|
||||
for rec in self:
|
||||
it = rec.input_type or 'text'
|
||||
rec.is_boolean_type = it in ('boolean', 'pass_fail')
|
||||
rec.is_date_type = it == 'date'
|
||||
rec.is_numeric_type = it in numeric_types
|
||||
rec.is_photo_type = it == 'photo'
|
||||
rec.is_multi_point_type = it == 'multi_point_thickness'
|
||||
rec.is_panel_type = it == 'bath_chemistry_panel'
|
||||
|
||||
def _has_value(self):
|
||||
self.ensure_one()
|
||||
if self.is_photo_type:
|
||||
return bool(self.photo_value)
|
||||
if self.is_multi_point_type:
|
||||
return any([
|
||||
self.point_1, self.point_2, self.point_3,
|
||||
self.point_4, self.point_5,
|
||||
])
|
||||
if self.is_panel_type:
|
||||
return any([
|
||||
self.panel_ph, self.panel_concentration,
|
||||
self.panel_temperature, self.panel_bath_id,
|
||||
])
|
||||
return any([
|
||||
self.value_text,
|
||||
self.value_number,
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<field name="is_boolean_type" column_invisible="1"/>
|
||||
<field name="is_date_type" column_invisible="1"/>
|
||||
<field name="is_numeric_type" column_invisible="1"/>
|
||||
<field name="is_photo_type" column_invisible="1"/>
|
||||
<field name="is_multi_point_type" column_invisible="1"/>
|
||||
<field name="is_panel_type" column_invisible="1"/>
|
||||
<field name="name"
|
||||
string="Measurement"
|
||||
readonly="is_authored"
|
||||
@@ -33,15 +36,6 @@
|
||||
string="Unit"
|
||||
readonly="is_authored"
|
||||
optional="show"/>
|
||||
<!-- Distinct column labels so the operator
|
||||
reads which input matches the row's
|
||||
type. List-view columns are static in
|
||||
Odoo — labelling each by its purpose
|
||||
removes the "four identical Value
|
||||
columns" guesswork from the previous
|
||||
layout. Only the cell matching the
|
||||
row's type stays editable; others sit
|
||||
blank. -->
|
||||
<field name="value_number"
|
||||
string="Number"
|
||||
invisible="not is_numeric_type"/>
|
||||
@@ -54,7 +48,33 @@
|
||||
invisible="not is_date_type"/>
|
||||
<field name="value_text"
|
||||
string="Text"
|
||||
invisible="is_numeric_type or is_boolean_type or is_date_type"/>
|
||||
invisible="is_numeric_type or is_boolean_type or is_date_type or is_photo_type or is_multi_point_type or is_panel_type"/>
|
||||
<field name="photo_value"
|
||||
string="Photo"
|
||||
widget="image"
|
||||
options="{'preview_image': 'photo_value'}"
|
||||
invisible="not is_photo_type"/>
|
||||
<field name="photo_filename" column_invisible="1"/>
|
||||
<field name="point_1" string="R1"
|
||||
invisible="not is_multi_point_type" optional="show"/>
|
||||
<field name="point_2" string="R2"
|
||||
invisible="not is_multi_point_type" optional="show"/>
|
||||
<field name="point_3" string="R3"
|
||||
invisible="not is_multi_point_type" optional="show"/>
|
||||
<field name="point_4" string="R4"
|
||||
invisible="not is_multi_point_type" optional="hide"/>
|
||||
<field name="point_5" string="R5"
|
||||
invisible="not is_multi_point_type" optional="hide"/>
|
||||
<field name="point_avg" string="Avg" readonly="1"
|
||||
invisible="not is_multi_point_type"/>
|
||||
<field name="panel_ph" string="pH"
|
||||
invisible="not is_panel_type"/>
|
||||
<field name="panel_concentration" string="Conc"
|
||||
invisible="not is_panel_type"/>
|
||||
<field name="panel_temperature" string="Temp"
|
||||
invisible="not is_panel_type"/>
|
||||
<field name="panel_bath_id" string="Bath"
|
||||
invisible="not is_panel_type"/>
|
||||
<field name="target_min" optional="hide"/>
|
||||
<field name="target_max" optional="hide"/>
|
||||
</list>
|
||||
|
||||
Reference in New Issue
Block a user