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:
@@ -103,6 +103,15 @@ class FpProcessNode(models.Model):
|
||||
string='Description',
|
||||
help='Rich text instructions for this step.',
|
||||
)
|
||||
# Sub 12d — master switch for runtime data collection. When False the
|
||||
# operator wizard skips this step entirely (no input prompts shown).
|
||||
collect_measurements = fields.Boolean(
|
||||
string='Collect Measurements at Runtime',
|
||||
default=True,
|
||||
help='Master switch. When off, the operator wizard skips this step '
|
||||
'entirely (no input prompts shown). Use for housekeeping steps '
|
||||
'or when no measurement is needed for this recipe.',
|
||||
)
|
||||
notes = fields.Text(
|
||||
string='Internal Notes',
|
||||
help='Internal notes (not shown to customers).',
|
||||
@@ -633,6 +642,10 @@ class FpProcessNodeInput(models.Model):
|
||||
('signature', 'Signature'),
|
||||
('location_picker', 'Location Picker'),
|
||||
('customer_wo', 'Customer WO #'),
|
||||
('photo', 'Photo'),
|
||||
('multi_point_thickness', 'Multi-Point Thickness (avg)'),
|
||||
('bath_chemistry_panel', 'Bath Chemistry Panel'),
|
||||
('ph', 'pH'),
|
||||
],
|
||||
string='Input Type',
|
||||
required=True,
|
||||
@@ -695,3 +708,21 @@ class FpProcessNodeInput(models.Model):
|
||||
],
|
||||
string='Compliance Tag', default='none',
|
||||
)
|
||||
|
||||
# ===== Sub 12d — per-recipe configurability =============================
|
||||
collect = fields.Boolean(
|
||||
string='Collect This Measurement',
|
||||
default=True,
|
||||
help='Toggle off to skip this prompt at runtime without deleting '
|
||||
'it. Recipe authors use this to opt out of library-seeded '
|
||||
'prompts without affecting the library itself.',
|
||||
)
|
||||
template_input_id = fields.Many2one(
|
||||
'fp.step.template.input',
|
||||
string='Source Library Prompt',
|
||||
ondelete='set null',
|
||||
help='Set when this row was snapshot-copied from a library template '
|
||||
'prompt. Powers "Reset to Library Defaults" — rows where this '
|
||||
'is False are treated as recipe-only custom prompts and survive '
|
||||
'the reset.',
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class FpStepTemplate(models.Model):
|
||||
@@ -75,22 +75,30 @@ class FpStepTemplate(models.Model):
|
||||
help='Opens the transition form before Mark Done (Sub 12b).')
|
||||
|
||||
default_kind = fields.Selection([
|
||||
('cleaning', 'Cleaning'),
|
||||
('etch', 'Etch'),
|
||||
('rinse', 'Rinse'),
|
||||
('plate', 'Plating'),
|
||||
('bake', 'Bake'),
|
||||
('inspect', 'Inspection'),
|
||||
('receiving', 'Receiving / Incoming Inspection'),
|
||||
('contract_review', 'Contract Review (QA-005)'),
|
||||
('racking', 'Racking'),
|
||||
('derack', 'De-Racking'),
|
||||
('mask', 'Masking'),
|
||||
('demask', 'De-Masking'),
|
||||
('dry', 'Drying'),
|
||||
('cleaning', 'Cleaning'),
|
||||
('electroclean', 'Electroclean'),
|
||||
('etch', 'Etch / Activation'),
|
||||
('rinse', 'Rinse'),
|
||||
('strike', 'Strike (Wood\'s Nickel / Activation)'),
|
||||
('plate', 'Plating'),
|
||||
('replenishment', 'Tank Replenishment'),
|
||||
('wbf_test', 'Water Break Free Test'),
|
||||
('dry', 'Drying'),
|
||||
('bake', 'Bake (HE Relief / Stress Relief)'),
|
||||
('demask', 'De-Masking'),
|
||||
('derack', 'De-Racking'),
|
||||
('inspect', 'Inspection'),
|
||||
('hardness_test', 'Hardness Test (HV / HK / HRC)'),
|
||||
('adhesion_test', 'Adhesion Test'),
|
||||
('salt_spray', 'Salt Spray / Corrosion Test'),
|
||||
('final_inspect', 'Final Inspection'),
|
||||
('packaging', 'Packaging / Pre-Ship'),
|
||||
('ship', 'Shipping'),
|
||||
('gating', 'Gating'),
|
||||
('contract_review', 'Contract Review (QA-005)'),
|
||||
], string='Step Kind', help='Drives sane-default input seeding.')
|
||||
|
||||
input_template_ids = fields.One2many(
|
||||
@@ -138,43 +146,183 @@ class FpStepTemplate(models.Model):
|
||||
# each → 'each', min → 'min'. Format-only strings ('HH:MM') get
|
||||
# left blank since they're not units.
|
||||
DEFAULT_INPUTS_BY_KIND = {
|
||||
'receiving': [
|
||||
{'name': 'Qty Received', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10, 'required': True},
|
||||
{'name': 'Qty Rejected', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 20},
|
||||
{'name': 'Customer PO# Verified', 'input_type': 'boolean', 'sequence': 30},
|
||||
{'name': 'Packing Slip #', 'input_type': 'text', 'sequence': 40},
|
||||
{'name': 'Condition Notes', 'input_type': 'text', 'sequence': 50},
|
||||
{'name': 'Damage Photo', 'input_type': 'photo', 'sequence': 60},
|
||||
{'name': 'Inspector Initials', 'input_type': 'signature',
|
||||
'sequence': 70, 'required': True},
|
||||
],
|
||||
'cleaning': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 30},
|
||||
{'name': 'Ultrasonic On', 'input_type': 'boolean', 'sequence': 40},
|
||||
{'name': 'Titration Done', 'input_type': 'boolean', 'sequence': 50},
|
||||
],
|
||||
'electroclean': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Amperage', 'input_type': 'number', 'sequence': 30,
|
||||
'hint': 'A'},
|
||||
{'name': 'Voltage', 'input_type': 'number', 'sequence': 40,
|
||||
'hint': 'V'},
|
||||
{'name': 'Current Density', 'input_type': 'number', 'sequence': 50,
|
||||
'hint': 'ASF (A per sq ft)'},
|
||||
{'name': 'Polarity', 'input_type': 'selection', 'sequence': 60,
|
||||
'selection_options': 'anodic,cathodic,periodic'},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 70},
|
||||
],
|
||||
'etch': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Acid Concentration', 'input_type': 'number', 'sequence': 30,
|
||||
'hint': '% or g/L'},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 40},
|
||||
{'name': 'HE Risk Flag', 'input_type': 'boolean', 'sequence': 50,
|
||||
'hint': 'Hydrogen Embrittlement risk for high-strength steel'},
|
||||
],
|
||||
'rinse': [
|
||||
{'name': 'Rinse Type', 'input_type': 'selection', 'sequence': 10,
|
||||
'selection_options': 'cascade,spray,DI,city'},
|
||||
{'name': 'Conductivity', 'input_type': 'number', 'sequence': 20,
|
||||
'hint': 'µS/cm — required for DI rinses'},
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 30},
|
||||
],
|
||||
'strike': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Amperage', 'input_type': 'number', 'sequence': 30,
|
||||
'hint': 'A'},
|
||||
{'name': 'Voltage', 'input_type': 'number', 'sequence': 40,
|
||||
'hint': 'V'},
|
||||
{'name': 'Current Density', 'input_type': 'number', 'sequence': 50,
|
||||
'hint': 'ASF'},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 60},
|
||||
],
|
||||
'rinse': [],
|
||||
'plate': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_hms',
|
||||
'target_unit': 'min', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Plating Thickness', 'input_type': 'thickness',
|
||||
'target_unit': 'in', 'sequence': 30},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 30},
|
||||
{'name': 'pH', 'input_type': 'ph', 'sequence': 40},
|
||||
{'name': 'Bath Concentration', 'input_type': 'number', 'sequence': 50,
|
||||
'hint': 'g/L'},
|
||||
{'name': 'Current Density', 'input_type': 'number', 'sequence': 60,
|
||||
'hint': 'ASF — electroplate only'},
|
||||
{'name': 'Plating Thickness', 'input_type': 'multi_point_thickness',
|
||||
'target_unit': 'in', 'sequence': 70},
|
||||
],
|
||||
'bake': [
|
||||
{'name': 'Time In', 'input_type': 'text', 'sequence': 10},
|
||||
{'name': 'Time Out', 'input_type': 'text', 'sequence': 20},
|
||||
'replenishment': [
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 10,
|
||||
'required': True},
|
||||
{'name': 'Chemistry Added', 'input_type': 'text', 'sequence': 20,
|
||||
'hint': 'name + amount, e.g. "Nickel sulfamate 500mL"'},
|
||||
{'name': 'pH Before', 'input_type': 'ph', 'sequence': 30},
|
||||
{'name': 'pH After', 'input_type': 'ph', 'sequence': 40},
|
||||
{'name': 'Concentration Before', 'input_type': 'number', 'sequence': 50},
|
||||
{'name': 'Concentration After', 'input_type': 'number', 'sequence': 60},
|
||||
{'name': 'Operator Initials', 'input_type': 'signature',
|
||||
'sequence': 70, 'required': True},
|
||||
],
|
||||
'wbf_test': [
|
||||
{'name': 'Result', 'input_type': 'pass_fail', 'sequence': 10,
|
||||
'required': True},
|
||||
{'name': 'Retest Count', 'input_type': 'number', 'sequence': 20},
|
||||
{'name': 'Photo on FAIL', 'input_type': 'photo', 'sequence': 30},
|
||||
],
|
||||
'dry': [
|
||||
{'name': 'Dry Method', 'input_type': 'selection', 'sequence': 10,
|
||||
'selection_options': 'hot air,oven,spin'},
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 20},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 30},
|
||||
],
|
||||
'bake': [
|
||||
{'name': 'Time In', 'input_type': 'date', 'sequence': 10},
|
||||
{'name': 'Time Out', 'input_type': 'date', 'sequence': 20},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 30},
|
||||
{'name': 'Oven ID', 'input_type': 'text', 'sequence': 40},
|
||||
{'name': 'Chart Recorder File', 'input_type': 'photo', 'sequence': 50,
|
||||
'hint': 'Attach AMS-2759 chart-recorder file'},
|
||||
],
|
||||
'racking': [
|
||||
{'name': 'Actual Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
'target_unit': 'each', 'sequence': 10, 'required': True},
|
||||
{'name': 'Rack ID', 'input_type': 'text', 'sequence': 20},
|
||||
{'name': 'Masking Applied', 'input_type': 'boolean', 'sequence': 30},
|
||||
{'name': 'Photo of Racked Load', 'input_type': 'photo', 'sequence': 40},
|
||||
],
|
||||
'derack': [
|
||||
{'name': 'Actual Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
{'name': 'Mask Removal Method', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'mechanical,solvent,thermal,not applicable'},
|
||||
{'name': 'Residue Check', 'input_type': 'pass_fail', 'sequence': 30},
|
||||
],
|
||||
'mask': [
|
||||
{'name': 'Actual Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
{'name': 'Mask Material', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'Microshield,latex tape,vinyl plugs,wax,other'},
|
||||
{'name': 'Photo of Masked Parts', 'input_type': 'photo', 'sequence': 30},
|
||||
],
|
||||
'demask': [
|
||||
{'name': 'Residue Check', 'input_type': 'pass_fail', 'sequence': 10},
|
||||
{'name': 'Surface Condition', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'clean,marks,needs rework'},
|
||||
],
|
||||
'inspect': [
|
||||
{'name': 'PASS/FAIL', 'input_type': 'pass_fail', 'sequence': 10},
|
||||
{'name': 'Result', 'input_type': 'pass_fail', 'sequence': 10,
|
||||
'required': True},
|
||||
{'name': 'Defect Type', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'pitting,burn,blister,peel,missing coverage,none'},
|
||||
{'name': 'Thickness Sample', 'input_type': 'thickness',
|
||||
'target_unit': 'in', 'sequence': 30},
|
||||
{'name': 'Photo', 'input_type': 'photo', 'sequence': 40},
|
||||
{'name': 'Inspector Signature', 'input_type': 'signature', 'sequence': 50},
|
||||
],
|
||||
'hardness_test': [
|
||||
{'name': 'Test Load', 'input_type': 'number', 'sequence': 10,
|
||||
'hint': 'gf'},
|
||||
{'name': 'Readings (HV/HK/HRC)', 'input_type': 'multi_point_thickness',
|
||||
'sequence': 20, 'hint': 'Three indents minimum'},
|
||||
{'name': 'Equipment ID', 'input_type': 'text', 'sequence': 30},
|
||||
{'name': 'Last Calibration Date', 'input_type': 'date', 'sequence': 40},
|
||||
],
|
||||
'adhesion_test': [
|
||||
{'name': 'Test Method', 'input_type': 'selection', 'sequence': 10,
|
||||
'selection_options': 'bend,tape,burnish,file'},
|
||||
{'name': 'Result', 'input_type': 'pass_fail', 'sequence': 20,
|
||||
'required': True},
|
||||
{'name': 'Photo of Coupon', 'input_type': 'photo', 'sequence': 30},
|
||||
],
|
||||
'salt_spray': [
|
||||
{'name': 'Test Duration', 'input_type': 'number', 'sequence': 10,
|
||||
'hint': 'hours'},
|
||||
{'name': 'Result', 'input_type': 'pass_fail', 'sequence': 20,
|
||||
'required': True},
|
||||
{'name': 'Red Rust %', 'input_type': 'number', 'sequence': 30},
|
||||
{'name': 'White Corrosion %', 'input_type': 'number', 'sequence': 40},
|
||||
{'name': 'Lab Report', 'input_type': 'photo', 'sequence': 50,
|
||||
'hint': 'Attach scanned lab report'},
|
||||
],
|
||||
'final_inspect': [
|
||||
{'name': 'Outgoing Part Count Verified',
|
||||
@@ -183,35 +331,80 @@ class FpStepTemplate(models.Model):
|
||||
'target_unit': 'each', 'sequence': 20},
|
||||
{'name': 'Qty Rejected', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 30},
|
||||
{'name': 'Defect Categorization', 'input_type': 'selection', 'sequence': 35,
|
||||
'selection_options': 'pitting,burn,blister,peel,missing coverage,dimensional,none'},
|
||||
{'name': 'Actual Coating Thickness',
|
||||
'input_type': 'thickness', 'target_unit': 'in', 'sequence': 40},
|
||||
{'name': 'Pass/Fail', 'input_type': 'pass_fail', 'sequence': 50},
|
||||
'input_type': 'multi_point_thickness',
|
||||
'target_unit': 'in', 'sequence': 40},
|
||||
{'name': 'Dimensional Verification', 'input_type': 'pass_fail',
|
||||
'sequence': 45},
|
||||
{'name': 'Surface Finish (Ra)', 'input_type': 'number', 'sequence': 47,
|
||||
'hint': 'µin'},
|
||||
{'name': 'Pass/Fail', 'input_type': 'pass_fail', 'sequence': 50,
|
||||
'required': True},
|
||||
{'name': 'Inspector Signature', 'input_type': 'signature', 'sequence': 60},
|
||||
],
|
||||
'wbf_test': [
|
||||
{'name': 'PASS/FAIL', 'input_type': 'pass_fail', 'sequence': 10},
|
||||
'packaging': [
|
||||
{'name': 'Packaging Type', 'input_type': 'selection', 'sequence': 10,
|
||||
'selection_options': 'VCI bag,bubble wrap,separator paper,custom crate,other'},
|
||||
{'name': 'Qty Per Package', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 20},
|
||||
{'name': 'Package Count', 'input_type': 'number', 'sequence': 30},
|
||||
{'name': 'Cert Package Included', 'input_type': 'boolean', 'sequence': 40},
|
||||
{'name': 'Customer-Supplied Packaging', 'input_type': 'boolean',
|
||||
'sequence': 50},
|
||||
],
|
||||
'mask': [
|
||||
{'name': 'Actual Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
],
|
||||
'demask': [],
|
||||
'dry': [],
|
||||
'ship': [
|
||||
{'name': 'Outgoing Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
'target_unit': 'each', 'sequence': 10, 'required': True},
|
||||
{'name': 'Carrier', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'UPS,FedEx,Purolator,Customer Pickup,Other'},
|
||||
{'name': 'Tracking #', 'input_type': 'text', 'sequence': 30},
|
||||
{'name': 'BoL #', 'input_type': 'text', 'sequence': 40},
|
||||
{'name': 'Photo of Sealed Shipment', 'input_type': 'photo',
|
||||
'sequence': 50},
|
||||
],
|
||||
'gating': [],
|
||||
# Sub 4 + 12c follow-up — Contract Review step (Policy B).
|
||||
# The shop-floor step itself is a tickbox; the heavy QA-005 form
|
||||
# is opened via fp.contract.review (separate model). These
|
||||
# inputs capture summary fields for the chronological CoC.
|
||||
'contract_review': [
|
||||
{'name': 'Reviewer Initials', 'input_type': 'text', 'sequence': 10},
|
||||
{'name': 'Reviewer Initials', 'input_type': 'signature', 'sequence': 10},
|
||||
{'name': 'Date Reviewed', 'input_type': 'date', 'sequence': 20},
|
||||
{'name': 'QA-005 Approved', 'input_type': 'pass_fail', 'sequence': 30},
|
||||
],
|
||||
}
|
||||
|
||||
COMMON_AUDIT_FIELDS = [
|
||||
{'name': 'Operator Initials', 'input_type': 'signature',
|
||||
'required': True, 'sequence': 800},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 810},
|
||||
{'name': 'Photo on Failure', 'input_type': 'photo', 'sequence': 820,
|
||||
'hint': 'upload only if failure observed'},
|
||||
{'name': 'Equipment ID', 'input_type': 'text', 'sequence': 830},
|
||||
]
|
||||
|
||||
def action_add_common_audit_fields(self):
|
||||
"""Idempotently append the common audit fields to this template.
|
||||
Skips rows whose name already exists. Logs to chatter.
|
||||
"""
|
||||
Input = self.env['fp.step.template.input']
|
||||
for tpl in self:
|
||||
existing_names = set(tpl.input_template_ids.mapped('name'))
|
||||
added = []
|
||||
for spec in self.COMMON_AUDIT_FIELDS:
|
||||
if spec['name'] in existing_names:
|
||||
continue
|
||||
Input.create({
|
||||
'template_id': tpl.id,
|
||||
**spec,
|
||||
})
|
||||
added.append(spec['name'])
|
||||
if added:
|
||||
tpl.message_post(
|
||||
body=_('Added common audit fields: %s') % ', '.join(added),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return True
|
||||
|
||||
def action_seed_default_inputs(self):
|
||||
"""Seed input_template_ids based on default_kind. Idempotent —
|
||||
only adds inputs whose names don't already exist on this template.
|
||||
|
||||
@@ -36,6 +36,10 @@ class FpStepTemplateInput(models.Model):
|
||||
('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'),
|
||||
], string='Input Type', required=True, default='text')
|
||||
target_min = fields.Float(string='Target Min',
|
||||
help='Lower bound of the acceptable range, expressed in Target Unit.')
|
||||
|
||||
Reference in New Issue
Block a user