# Step Library Expansion + Per-Recipe Configurability + Audit Coverage — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Cover the full plating-shop workflow with new Step Kinds + audit-grade default measurements, expose per-recipe configurability for every prompt + master toggle + custom prompts + reset-to-library, surface office-to-operator instructions in the simple recipe editor with library/recipe override, wire all of it through to runtime + CoC report + tablet, and battle-test end-to-end on entech.
**Architecture:** Library `fp.step.template` is the smart default; recipe `fusion.plating.process.node` is the final say. New Selection values added to step kinds + input types. New per-row `collect` boolean (with master `collect_measurements` on the node) drives runtime filtering. Office instructions stay as `description` Html on both library and recipe; recipe override falls through to library when empty. Battle test exercises the full chain library → recipe → job → CoC report.
**Tech Stack:** Odoo 19, Python (models, computes, migrations), OWL (recipe editor), QWeb (CoC report), JSON-RPC controllers.
**Spec:** [`docs/superpowers/specs/2026-04-29-step-library-audit-design.md`](../specs/2026-04-29-step-library-audit-design.md)
**Deploy target:** entech (LXC 111 on pve-worker5, native Odoo, DB `admin`, addons at `/mnt/extra-addons/custom/`).
---
## File Map
| File | Status | Responsibility |
|---|---|---|
| `fusion_plating/models/fp_step_template.py` | MOD | Extend `default_kind`, expand `DEFAULT_INPUTS_BY_KIND`, add `action_add_common_audit_fields` |
| `fusion_plating/models/fp_step_template_input.py` | MOD | Add 4 new input types to selection |
| `fusion_plating/models/fp_process_node.py` | MOD | Add `collect_measurements` on node; add `collect` + `template_input_id` on input model; mirror new input types |
| `fusion_plating/views/fp_step_template_views.xml` | MOD | Add audit-fields button; relabel description |
| `fusion_plating/data/fp_step_template_data.xml` | NEW | Seed templates for the 8 new Step Kinds |
| `fusion_plating/migrations/19.0.18.7.0/post-migrate.py` | NEW | Backfill collect/collect_measurements; re-seed defaults |
| `fusion_plating/static/src/js/simple_recipe_editor.js` | MOD | Render Instructions + Measurements expansions; collect badge |
| `fusion_plating/static/src/xml/simple_recipe_editor.xml` | MOD | OWL templates for new affordances |
| `fusion_plating/static/src/scss/simple_recipe_editor.scss` | MOD | Styles for expansions and badges |
| `fusion_plating/controllers/simple_recipe_controller.py` | MOD | New endpoints for toggle/edit/reset |
| `fusion_plating_jobs/wizards/fp_job_step_input_wizard.py` | MOD | Filter `collect=True`; mirror new input types |
| `fusion_plating_jobs/wizards/fp_job_step_input_wizard_views.xml` | MOD | Per-input-type widgets |
| `fusion_plating_shopfloor/static/src/...` (tablet OWL) | MOD | Per-type rendering |
| `fusion_plating_reports/views/report_coc_chronological.xml` | MOD | Render branches for new types; filter `collect=True` |
| `fusion_plating/scripts/bt_step_library_audit.py` | NEW | 18-assertion battle-test |
| `fusion_plating/__manifest__.py` | MOD | Bump to `19.0.18.7.0` |
| `fusion_plating_jobs/__manifest__.py` | MOD | Bump version |
| `fusion_plating_reports/__manifest__.py` | MOD | Bump version |
---
## Phase A — Model Foundations
### Task A1: Add new input types to library template input model
**Files:**
- Modify: `fusion_plating/models/fp_step_template_input.py`
- [ ] **Step 1: Open the file and locate the `input_type` Selection at line ~27**
- [ ] **Step 2: Extend the selection with 4 new types**
Replace:
```python
input_type = fields.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'),
], string='Input Type', required=True, default='text')
```
With:
```python
input_type = fields.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'),
('photo', 'Photo'),
('multi_point_thickness', 'Multi-Point Thickness (avg)'),
('bath_chemistry_panel', 'Bath Chemistry Panel'),
('ph', 'pH'),
], string='Input Type', required=True, default='text')
```
- [ ] **Step 3: Verify Python parses cleanly**
Run: `python -c "import ast; ast.parse(open('K:/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_step_template_input.py').read())"`
Expected: silent (no error).
### Task A2: Mirror new input types on the recipe-step (process node) input model
**Files:**
- Modify: `fusion_plating/models/fp_process_node.py:621-640` (the `input_type` Selection on the inline input model)
- [ ] **Step 1: Locate the input_type Selection block (search for `('thickness', 'Thickness')`)**
- [ ] **Step 2: Add the four new entries above the closing bracket**
Replace:
```python
input_type = fields.Selection(
[
('text', 'Text'),
('number', 'Number'),
('boolean', 'Yes/No'),
('selection', 'Selection'),
('time_hms', 'Time (HH:MM:SS)'),
('time_seconds', 'Time (seconds)'),
('temperature', 'Temperature'),
('thickness', 'Thickness'),
('pass_fail', 'Pass / Fail'),
('date', 'Date / Time'),
('signature', 'Signature'),
('location_picker', 'Location Picker'),
('customer_wo', 'Customer WO #'),
],
string='Input Type',
required=True,
default='text',
)
```
With:
```python
input_type = fields.Selection(
[
('text', 'Text'),
('number', 'Number'),
('boolean', 'Yes/No'),
('selection', 'Selection'),
('time_hms', 'Time (HH:MM:SS)'),
('time_seconds', 'Time (seconds)'),
('temperature', 'Temperature'),
('thickness', 'Thickness'),
('pass_fail', 'Pass / Fail'),
('date', 'Date / Time'),
('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,
default='text',
)
```
### Task A3: Add `collect`, `template_input_id`, `collect_measurements` fields
**Files:**
- Modify: `fusion_plating/models/fp_process_node.py`
- [ ] **Step 1: Locate the inline input model class (it's a nested model inside fp_process_node.py — search for `_name = 'fusion.plating.process.node.input'`).**
- [ ] **Step 2: Add `collect` and `template_input_id` after the `compliance_tag` field (around line 698)**
Insert immediately before the class' closing `_sql_constraints` or the next field block:
```python
# ===== 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.',
)
```
- [ ] **Step 3: Add `collect_measurements` boolean on the parent node model**
Locate the main `fusion.plating.process.node` model class (search for `_name = 'fusion.plating.process.node'` — the node itself, not the input child). Find a sensible location near the existing `description` field (search for `description = fields.Html`) and add immediately after:
```python
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.',
)
```
- [ ] **Step 4: Verify Python parses**
Run: `python -c "import ast; ast.parse(open('K:/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_process_node.py').read())"`
Expected: silent.
- [ ] **Step 5: Commit Phase A**
```bash
cd K:/Github/Odoo-Modules/fusion_plating
git add fusion_plating/models/fp_step_template_input.py fusion_plating/models/fp_process_node.py
git commit -m "model(step-library): add 4 new input types + per-recipe collect toggles"
```
---
## Phase B — Library DEFAULT_INPUTS_BY_KIND + 8 New Step Kinds
### Task B1: Add 8 new Step Kinds to default_kind Selection
**Files:**
- Modify: `fusion_plating/models/fp_step_template.py:77-94`
- [ ] **Step 1: Locate the `default_kind` Selection field (around line 77)**
- [ ] **Step 2: Replace it with the expanded list**
Replace:
```python
default_kind = fields.Selection([
('cleaning', 'Cleaning'),
('etch', 'Etch'),
('rinse', 'Rinse'),
('plate', 'Plating'),
('bake', 'Bake'),
('inspect', 'Inspection'),
('racking', 'Racking'),
('derack', 'De-Racking'),
('mask', 'Masking'),
('demask', 'De-Masking'),
('dry', 'Drying'),
('wbf_test', 'Water Break Free Test'),
('final_inspect', 'Final Inspection'),
('ship', 'Shipping'),
('gating', 'Gating'),
('contract_review', 'Contract Review (QA-005)'),
], string='Step Kind', help='Drives sane-default input seeding.')
```
With:
```python
default_kind = fields.Selection([
('receiving', 'Receiving / Incoming Inspection'),
('contract_review', 'Contract Review (QA-005)'),
('racking', 'Racking'),
('mask', 'Masking'),
('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'),
], string='Step Kind', help='Drives sane-default input seeding.')
```
The list is now ordered roughly by typical workflow phase to make it easier for shop authors to scan.
### Task B2: Expand DEFAULT_INPUTS_BY_KIND for all kinds
**Files:**
- Modify: `fusion_plating/models/fp_step_template.py:140-213`
- [ ] **Step 1: Replace the `DEFAULT_INPUTS_BY_KIND` dict with the full expanded version**
Replace the entire dict (lines 140 through the closing `}` around line 213) with:
```python
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},
],
'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': '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},
],
'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, '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': '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',
'input_type': 'boolean', 'sequence': 10},
{'name': 'Qty Accepted', 'input_type': 'number',
'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': '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},
],
'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},
],
'ship': [
{'name': 'Outgoing Qty', 'input_type': 'number',
'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': [],
'contract_review': [
{'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},
],
}
```
### Task B3: Add `action_add_common_audit_fields` method
**Files:**
- Modify: `fusion_plating/models/fp_step_template.py` (after `action_seed_default_inputs`, around line 234)
- [ ] **Step 1: Append after `action_seed_default_inputs`**
```python
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
```
- [ ] **Step 2: Verify Python parses**
Run: `python -c "import ast; ast.parse(open('K:/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_step_template.py').read())"`
Expected: silent.
- [ ] **Step 3: Commit Phase B model changes**
```bash
git add fusion_plating/models/fp_step_template.py
git commit -m "model(step-library): add 8 new Step Kinds + expanded defaults + audit-fields one-click"
```
---
## Phase C — Library Views
### Task C1: Surface "Add Common Audit Fields" button + relabel description
**Files:**
- Modify: `fusion_plating/views/fp_step_template_views.xml`
- [ ] **Step 1: In the form view's `