Tightened from the original 18-task plan after inspecting existing templates: - report_coc_en / report_coc_fr already exist with Nadcap/AS9100/CGP logos, signature, certified_by — solid. Add a chronological body alongside, don't rebuild. - company.x_fc_nadcap_logo etc already exist on res.company. Skip. - The native fp.job traveller is minimal (post-Sub-11) and needs the paper-style upgrade. Replace its body, not the action. - fp.job.step.timelog state machine landed in Sub 12b — Sub 12c just ships views + menu. 5-task breakdown: 1. Bump versions + manifest scaffolding 2. Operator Traveller v2 (A4 landscape, paper-style, target columns) 3. Chronological CoC body + body_style opt-in router 4. Labor History list/form/search + Plating menu 5. Deploy to entech + smoke test Out of scope: rack travel ticket PDF (Sub 12b's Save+Print 404 stays flagged), per-customer cert statement (boilerplate inline for now). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 KiB
Sub 12c — Operator Traveller v2 + Chronological CoC + Labor History
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: Upgrade the operator traveller PDF to paper-style A4 landscape (matching the Amphenol screens 16-18), add a chronological body to the existing CoC report (walks fp.job.step.move in time order), and ship a Labor History screen for billing/payroll audit.
Architecture: Replace the minimal report_fp_job_traveller_template body with the paper-style table. Add a new coc_chronological_body QWeb template alongside the existing coc_body in fusion_plating_reports; introduce a body_style selection on fp.certificate so customers opt in per cert. Labor History = standard list/form/search views on the existing fp.job.step.timelog (state machine added by Sub 12b). No new models.
Tech Stack: Odoo 19, QWeb XML, SCSS. No JS. No new Python models.
Companion docs:
- Spec section 6
- Steelhead screen inventory — screens 16-24
Existing artifacts to extend (do NOT replace):
fusion_plating_jobs/report/report_fp_job_traveller.xml— native fp.job traveller (minimal, post-Sub-11). Body upgrade.fusion_plating_reports/report/report_coc.xml—coc_bodytemplate +report_coc_en/report_coc_fractions. Add a chronological body template; existing classic body untouched.fp.job.step.timelog— Sub 12b added the state machine. Sub 12c adds list/form/search views.
Out of scope (deferred):
- Rack travel ticket PDF (referenced by Sub 12b's Rack Parts Save+Print — keep as 404 placeholder, ship in a follow-up sub).
- New cert types / Nadcap rules — existing CoC infrastructure already handles them.
Deploy target: entech (LXC 111). -u --stop-after-init clean upgrade per task.
File structure
Files to create
fusion_plating/views/fp_job_step_timelog_views.xml # list/form/search + Labor History menu
fusion_plating_reports/report/report_coc_chronological.xml # new chronological CoC body template
Files to modify
fusion_plating/__manifest__.py # 19.0.10.1.0 → 19.0.10.2.0; add timelog views to data
fusion_plating_jobs/__manifest__.py # version bump
fusion_plating_jobs/report/report_fp_job_traveller.xml # rewrite template body to paper-style landscape
fusion_plating_reports/__manifest__.py # version bump; add report_coc_chronological.xml
fusion_plating_reports/report/report_coc.xml # extend coc_body to support body_style routing (optional minimal change)
fusion_plating_certificates/models/fp_certificate.py # add body_style selection field
fusion_plating_certificates/views/fp_certificate_views.xml # surface body_style on form
Conventions
- Read every file before editing. The CoC template has 250+ lines of carefully-tuned QWeb — don't restructure unless necessary.
- Headers on all new files: Copyright 2026 Nexa Systems Inc., OPL-1, Part of Fusion Plating.
- Verification: entech
-u --stop-after-initclean upgrade. Visual smoke test on a real job's traveller and a real cert's CoC.
Task 1: Bump versions + manifest data entries
Files:
-
Modify:
fusion_plating/__manifest__.py -
Modify:
fusion_plating_jobs/__manifest__.py -
Modify:
fusion_plating_reports/__manifest__.py -
Step 1: fusion_plating bump + add timelog views
'version': '19.0.10.1.0' → '19.0.10.2.0',
Add to 'data' list (after views/fp_job_step_move_views.xml):
'views/fp_job_step_timelog_views.xml',
- Step 2: fusion_plating_jobs bump
Read current version, bump patch.
- Step 3: fusion_plating_reports bump + add chronological CoC template
Read current version, bump patch. Add to 'data' list (after report_coc.xml):
'report/report_coc_chronological.xml',
- Step 4: Commit
git add fusion_plating/__manifest__.py \
fusion_plating_jobs/__manifest__.py \
fusion_plating_reports/__manifest__.py
git commit -m "feat(sub12c): bump versions + manifest scaffolding
fusion_plating → 19.0.10.2.0 (Labor History views)
fusion_plating_jobs → next patch (Operator Traveller v2 body)
fusion_plating_reports → next patch (Chronological CoC body template)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
Task 2: Operator Traveller v2 — paper-style A4 landscape
Files:
-
Modify:
fusion_plating_jobs/report/report_fp_job_traveller.xml -
Step 1: Read the current template
cat fusion_plating_jobs/report/report_fp_job_traveller.xml
- Step 2: Rewrite template + action
Replace the entire template body with the paper-style version below. The action stays at fusion_plating_jobs.report_fp_job_traveller_template so existing button bindings keep working.
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12c v2 — paper-style A4 landscape job traveller.
Mirrors the Amphenol Canada paper sheets (Steelhead screens 16-18):
barcode + WO header, item-info block, recipe sub-process header, then
the routing table with target ranges + actuals + sign-off cells per
step. Operators print one of these per job, pencil in actuals, then
the tablet captures the same data digitally — printed traveller is
the redundant audit copy.
-->
<odoo>
<record id="paperformat_fp_traveller_landscape" model="report.paperformat">
<field name="name">FP Traveller — A4 landscape narrow margins</field>
<field name="format">A4</field>
<field name="orientation">Landscape</field>
<field name="margin_top">10</field>
<field name="margin_bottom">10</field>
<field name="margin_left">8</field>
<field name="margin_right">8</field>
<field name="header_spacing">5</field>
<field name="dpi">90</field>
</record>
<record id="action_report_fp_job_traveller" model="ir.actions.report">
<field name="name">Job Traveller</field>
<field name="model">fp.job</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_jobs.report_fp_job_traveller_template</field>
<field name="report_file">fusion_plating_jobs.report_fp_job_traveller_template</field>
<field name="print_report_name">'Traveller - %s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="fusion_plating.model_fp_job"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_traveller_landscape"/>
</record>
<template id="report_fp_job_traveller_template">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="job">
<t t-call="web.external_layout">
<div class="page fp-trav-page">
<style>
.fp-trav-page { font-family: Arial, sans-serif; font-size: 8pt; color: #000; }
.fp-trav-page h1 { font-size: 14pt; margin: 0; }
.fp-trav-page h2 { font-size: 10pt; margin: 6px 0 2px 0; }
.fp-trav-page table.bordered,
.fp-trav-page table.bordered th,
.fp-trav-page table.bordered td { border: 1px solid #000; border-collapse: collapse; }
.fp-trav-page table.bordered th { background: #ededed; padding: 4px 6px; text-align: left; font-weight: bold; }
.fp-trav-page table.bordered td { padding: 4px 6px; vertical-align: top; }
.fp-trav-page .fp-trav-actuals { font-size: 7.5pt; color: #555; line-height: 1.5; }
.fp-trav-page .fp-trav-target { color: #444; font-size: 7.5pt; }
.fp-trav-page .fp-trav-blank { display: inline-block; min-width: 32mm; border-bottom: 1px solid #888; height: 1.2em; }
.fp-trav-page .fp-trav-stamp { min-height: 12mm; }
</style>
<!-- HEADER -->
<table class="bordered" style="width: 100%;">
<tr>
<td style="width: 5%; vertical-align: middle; text-align: center;">
<img t-if="job.company_id.logo"
t-att-src="'data:image/png;base64,%s' % job.company_id.logo.decode()"
style="max-width: 28mm; max-height: 18mm;"/>
</td>
<td colspan="2" style="vertical-align: middle;">
<h1>Work Order / Bon de Travail</h1>
<div style="text-align: center; margin-top: 4px;">
<strong t-esc="job.name"/>
</div>
<div style="text-align: center;">
<img t-att-src="'/report/barcode/Code128/%s' % job.name"
style="height: 14mm;"/>
</div>
</td>
<td style="width: 18%;">
<strong>Date In:</strong>
<span t-esc="job.create_date and job.create_date.strftime('%d-%m-%Y') or '—'"/><br/>
<strong>Due Date:</strong>
<span t-esc="job.date_deadline and job.date_deadline.strftime('%d-%m-%Y') or '—'"/><br/>
<strong>Type:</strong>
<span t-esc="job.recipe_id.name or '—'"/>
</td>
<td style="width: 18%;">
<strong>Order #:</strong>
<span t-esc="job.sale_order_id.name or '—'"/><br/>
<strong>P.O. #:</strong>
<span t-esc="job.sale_order_id.client_order_ref or '—'"/><br/>
<strong>WO Generated By:</strong>
<span t-esc="job.create_uid.name or '—'"/>
</td>
<td style="width: 22%; vertical-align: top;">
<strong t-esc="job.partner_id.name or '—'"/><br/>
<span t-esc="job.partner_id.street or ''"/><br/>
<span t-esc="(job.partner_id.city or '') + ', ' + (job.partner_id.state_id.code or '') + ' ' + (job.partner_id.zip or '')"/><br/>
<strong>Tel:</strong> <span t-esc="job.partner_id.phone or '—'"/>
</td>
</tr>
</table>
<!-- ITEM INFORMATION -->
<table class="bordered" style="width: 100%; margin-top: 4px;">
<tr>
<th style="width: 22%;">Item Information</th>
<th style="width: 30%;">Item-Name / Process Description</th>
<th style="width: 8%;">Qty Rec.</th>
<th style="width: 6%;">Vis Insp</th>
<th style="width: 6%;">Rework</th>
<th style="width: 22%;">Special Requirements</th>
<th style="width: 6%;">Stamp / Date</th>
</tr>
<tr>
<td>
<strong>Part #:</strong> <span t-esc="job.part_catalog_id.part_number or '—'"/><br/>
<strong>Rev:</strong> <span t-esc="job.part_catalog_id.revision or '—'"/><br/>
<strong>Mat:</strong>
<t t-if="'base_material' in job.part_catalog_id._fields">
<span t-esc="job.part_catalog_id.base_material or '—'"/>
</t>
<t t-else=""><span>—</span></t><br/>
<strong>Catg:</strong> <span t-esc="job.recipe_id.name or '—'"/><br/>
<strong>S/N:</strong> <span t-esc="job.serial_number or ''"/>
</td>
<td>
<strong t-esc="job.part_catalog_id.name or job.product_id.name or '—'"/>
<div style="font-size: 7.5pt; margin-top: 2px;">
<t t-if="'customer_facing_description' in job.part_catalog_id._fields">
<span t-esc="job.part_catalog_id.customer_facing_description or ''"
style="white-space: pre-wrap;"/>
</t>
</div>
</td>
<td class="text-center">
<span t-esc="job.qty_received or job.qty"/>
</td>
<td class="text-center">
<span t-esc="job.qty_visual_inspection_rejects or 0"/>
</td>
<td class="text-center">
<span t-esc="job.qty_rework or 0"/>
</td>
<td style="font-size: 7pt; white-space: pre-wrap;">
<span t-esc="job.special_requirements or '—'"/>
</td>
<td class="fp-trav-stamp"/>
</tr>
</table>
<!-- PROCESS-SHEET HEADER -->
<table class="bordered" style="width: 100%; margin-top: 4px;">
<tr>
<th style="width: 30%;">Process Sheet / Feuille de Procédé</th>
<th style="width: 20%;">Catg.</th>
<th style="width: 50%;">Spec / Info</th>
</tr>
<tr>
<td><span t-esc="job.recipe_id.name or '—'"/></td>
<td><span t-esc="(job.recipe_id.process_type_id and job.recipe_id.process_type_id.name) or '—'"/></td>
<td>
<span t-esc="(job.coating_config_id and job.coating_config_id.name) or ''"/>
</td>
</tr>
</table>
<!-- ROUTING TABLE -->
<table class="bordered" style="width: 100%; margin-top: 4px;">
<thead>
<tr>
<th style="width: 3%;">Step</th>
<th style="width: 6%;">Tank</th>
<th style="width: 22%;">Operation + Actuals</th>
<th style="width: 22%;">Instruction</th>
<th style="width: 5%;">Unit</th>
<th style="width: 8%;">Material</th>
<th style="width: 6%;">Voltage</th>
<th style="width: 7%;">Time (min)</th>
<th style="width: 7%;">Temp</th>
<th style="width: 6%;">Stamp</th>
<th style="width: 8%;">Date</th>
</tr>
</thead>
<tbody>
<t t-foreach="job.step_ids.sorted('sequence')" t-as="step">
<t t-set="rn" t-value="step.recipe_node_id"/>
<tr>
<td class="text-center"><span t-esc="step_index + 1"/></td>
<td class="text-center"><span t-esc="(step.tank_id and step.tank_id.code) or '—'"/></td>
<td>
<strong t-esc="step.name"/>
<div class="fp-trav-actuals">
<t t-foreach="rn.input_ids.filtered(lambda i: i.kind == 'step_input').sorted('sequence')" t-as="inp">
<span t-esc="inp.name"/>:
<span class="fp-trav-blank"/>
<t t-if="inp.target_unit"> <span t-esc="inp.target_unit"/></t><br/>
</t>
</div>
</td>
<td style="font-size: 7.5pt; white-space: pre-wrap;">
<span t-esc="step.description or (rn and rn.description) or ''" t-options="{'widget': 'html'}"/>
</td>
<td class="text-center fp-trav-target">
<t t-if="rn and 'time_unit' in rn._fields and rn.time_unit">
<span t-esc="rn.time_unit"/>
</t>
<t t-else="">—</t>
</td>
<td class="text-center fp-trav-target">
<t t-if="rn and 'material_callout' in rn._fields and rn.material_callout">
<span t-esc="rn.material_callout"/>
</t>
<t t-elif="rn and rn.process_type_id">
<span t-esc="rn.process_type_id.name"/>
</t>
<t t-else="">N/A</t>
</td>
<td class="text-center fp-trav-target">
<t t-if="rn and 'voltage_target' in rn._fields and rn.voltage_target">
<span t-esc="rn.voltage_target"/>V
</t>
<t t-else="">N/A</t>
</td>
<td class="text-center fp-trav-target">
<t t-if="rn and 'time_min_target' in rn._fields and rn.time_max_target">
<span t-esc="rn.time_min_target"/> - <span t-esc="rn.time_max_target"/>
</t>
<t t-else="">N/A</t>
</td>
<td class="text-center fp-trav-target">
<t t-if="rn and 'temp_min_target' in rn._fields and rn.temp_max_target">
<span t-esc="rn.temp_min_target"/>-<span t-esc="rn.temp_max_target"/>
<span t-esc="rn.temp_unit"/>
</t>
<t t-else="">N/A</t>
</td>
<td class="fp-trav-stamp"/>
<td class="fp-trav-stamp"/>
</tr>
</t>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
</odoo>
- Step 3: Commit
git add fusion_plating_jobs/report/report_fp_job_traveller.xml
git commit -m "feat(sub12c): operator traveller v2 — paper-style A4 landscape (Task 2)
Replaces the minimal portrait template with the Amphenol-style paper
sheet (screens 16-18). Header: barcode (Code 128 via /report/barcode),
WO# / Date In / Due Date / Type / Order# / PO# / WO-Generated-By /
customer block with address. Item Information panel: Part# / Rev / Mat /
Catg / S/N + multi-line Item-Name + Qty Rec / VIS INSP / Rework / Special
Requirements / Stamp-Date.
Process-Sheet header: recipe name + category + spec/info.
Routing table: Step / Tank / Operation+Actuals (recipe inputs render
as 'Actual <name>: ____ unit' lines) / Instruction / Unit / Material /
Voltage / Time(min) / Temp / Stamp / Date. Targets pulled from recipe-
node fields when present (Sub 12a authored), N/A otherwise.
New paperformat: A4 landscape narrow margins, 90 dpi.
Action ID + report_name unchanged so existing form-button bindings keep
working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
Task 3: Customer CoC — chronological body template
Files:
-
Create:
fusion_plating_reports/report/report_coc_chronological.xml -
Modify:
fusion_plating_certificates/models/fp_certificate.py -
Modify:
fusion_plating_certificates/views/fp_certificate_views.xml -
Step 1: Add
body_stylefield onfp.certificate
In fp_certificate.py, find a clean place to add new fields (after the existing certified_by_id):
# ===== Sub 12c — chronological CoC opt-in =================================
body_style = fields.Selection(
[
('classic', 'Classic (recipe-order)'),
('chronological', 'Chronological (chain-of-custody)'),
],
string='CoC Body Style', default='classic',
help='Chronological walks fp.job.step.move records in time order '
'with measurement sub-tables per move, matching Steelhead\'s '
'CoC PDF layout. Classic uses the existing recipe-order body.',
)
- Step 2: Surface
body_styleon the cert form
In fp_certificate_views.xml, find the existing form view's group block and add:
<field name="body_style"/>
near the other certification settings.
- Step 3: Create the chronological body template
fusion_plating_reports/report/report_coc_chronological.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12c — Chronological CoC body.
Walks fp.job.step.move records in time order (chain-of-custody),
rendering each transition as a heading ("Step Name (Tank Code)")
with "Moved By / Time" + a 5-column measurement sub-table when the
destination step has captured input values. Mirrors Steelhead's
CoC PDF layout (screens 19-24).
Wired into the existing CoC actions via a `body_style='chronological'`
flag on fp.certificate — when set, action_report_coc_en/_fr render
this body instead of the classic recipe-order body.
-->
<odoo>
<template id="coc_body_chronological">
<t t-set="job" t-value="doc.x_fc_job_id if 'x_fc_job_id' in doc._fields else False"/>
<t t-set="moves" t-value="job.move_ids.sorted('move_datetime') if job and 'move_ids' in job._fields else []"/>
<style>
.fp-coc-chrono { font-family: Arial, sans-serif; font-size: 9pt; color: #000; padding-top: 8mm; }
.fp-coc-chrono h1 { text-align: center; font-size: 18pt; margin: 0 0 6px 0; }
.fp-coc-chrono h3 { font-size: 11pt; margin: 8px 0 2px 0; font-weight: bold; }
.fp-coc-chrono .fp-chrono-meta { font-size: 8.5pt; color: #444; margin-bottom: 4px; }
.fp-coc-chrono table.bordered,
.fp-coc-chrono table.bordered th,
.fp-coc-chrono table.bordered td { border: 1px solid #000; border-collapse: collapse; }
.fp-coc-chrono table.bordered { width: 100%; margin-bottom: 8px; }
.fp-coc-chrono table.bordered th { background: #ededed; padding: 4px 6px; font-size: 8.5pt; }
.fp-coc-chrono table.bordered td { padding: 4px 6px; vertical-align: top; font-size: 8.5pt; }
.fp-coc-chrono .fp-out-of-range { color: #b30000; font-weight: bold; }
.fp-coc-chrono .fp-in-range { color: #006400; }
.fp-coc-chrono .fp-pass { color: #006400; font-weight: bold; }
.fp-coc-chrono .fp-fail { color: #b30000; font-weight: bold; }
</style>
<div class="fp-coc-chrono">
<h1>Certificate of Conformance</h1>
<!-- Job header (compact) -->
<table class="bordered">
<tr>
<th style="width: 18%;">Part Number</th>
<th style="width: 30%;">Description</th>
<th style="width: 8%;">Quantity</th>
<th style="width: 8%;">Work Order</th>
<th style="width: 14%;">PO Number</th>
<th style="width: 12%;">Packing List No</th>
<th style="width: 10%;">Date</th>
</tr>
<tr>
<td><span t-esc="(job and job.part_catalog_id and job.part_catalog_id.part_number) or (job and job.product_id.default_code) or '—'"/></td>
<td><span t-esc="(job and job.part_catalog_id and job.part_catalog_id.name) or (job and job.product_id.name) or '—'"/></td>
<td class="text-center"><span t-esc="(job and job.qty) or ''"/></td>
<td class="text-center"><span t-esc="(job and job.name) or '—'"/></td>
<td><span t-esc="(job and job.sale_order_id and job.sale_order_id.client_order_ref) or '—'"/></td>
<td/>
<td><span t-esc="(doc.create_date and doc.create_date.strftime('%Y-%m-%d')) or ''"/></td>
</tr>
</table>
<h3 style="margin-top: 6px;">Specification(s):
<span style="font-weight: normal;"
t-esc="(job and job.recipe_id and job.recipe_id.name) or '—'"/>
</h3>
<hr style="border: 0; border-top: 2px solid #000; margin: 8px 0;"/>
<!-- Chain-of-custody walk -->
<t t-foreach="moves" t-as="mv">
<t t-set="dest" t-value="mv.to_step_id"/>
<t t-set="tank_code" t-value="mv.to_tank_id.code or (dest and dest.tank_id and dest.tank_id.code) or ''"/>
<t t-set="captured" t-value="dest.input_ids.filtered(lambda i: i.kind == 'step_input').sorted('sequence') if dest else []"/>
<h3>
<span t-esc="dest and dest.name or '—'"/>
<t t-if="tank_code"> (<span t-esc="tank_code"/>)</t>
</h3>
<div class="fp-chrono-meta">
<strong>Moved By:</strong> <span t-esc="mv.moved_by_user_id.name"/>
·
<strong>Time:</strong>
<span t-esc="mv.move_datetime and mv.move_datetime.strftime('%b %d, %Y %I:%M:%S %p') or ''"/>
<t t-if="mv.qty_moved">
· <strong>Qty:</strong> <span t-esc="mv.qty_moved"/>
</t>
</div>
<!-- Measurement sub-table — only render when captured input values exist on the destination step -->
<t t-if="captured">
<table class="bordered">
<thead>
<tr>
<th style="width: 24%;">Name</th>
<th style="width: 30%;">Description</th>
<th style="width: 14%;">Target</th>
<th style="width: 18%;">Actual</th>
<th style="width: 14%;">Recorded By</th>
</tr>
</thead>
<tbody>
<t t-foreach="captured" t-as="inp">
<!-- Pull captured value via fp.job.step.input.value
if Sub 12a wired one. For now, the runtime
captures into transition_input_value_ids on
the move (Sub 12b) — step inputs that
are recorded *during* the step still go in
a step-level table. We render the prompt
name + target here as the audit row;
`Actual` is blank if no capture. -->
<tr>
<td><span t-esc="inp.name"/></td>
<td><span t-esc="inp.hint or ''"/></td>
<td>
<t t-if="inp.target_min and inp.target_max">
<span t-esc="inp.target_min"/>–<span t-esc="inp.target_max"/>
<t t-if="inp.target_unit"> <span t-esc="inp.target_unit"/></t>
</t>
<t t-elif="inp.target_unit">
<span t-esc="inp.target_unit"/>
</t>
</td>
<td/>
<td><span t-esc="(mv.moved_by_user_id.name) or ''"/></td>
</tr>
</t>
</tbody>
</table>
</t>
</t>
<hr style="border: 0; border-top: 2px solid #000; margin: 12px 0;"/>
<!-- Sign-off block (re-uses owner_user_id signature pattern) -->
<t t-set="owner_sig" t-value="False"/>
<t t-if="company.x_fc_owner_user_id">
<t t-set="_emp" t-value="company.x_fc_owner_user_id.employee_ids[:1]"/>
<t t-if="_emp and 'signature' in _emp._fields">
<t t-set="owner_sig" t-value="_emp['signature']"/>
</t>
</t>
<t t-set="signature_img" t-value="company.x_fc_coc_signature_override or owner_sig"/>
<t t-set="signer_name" t-value="(doc.certified_by_id and doc.certified_by_id.name) or (company.x_fc_owner_user_id and company.x_fc_owner_user_id.name) or ''"/>
<table class="bordered" style="width: 100%;">
<tr>
<td style="width: 50%; vertical-align: top;">
<strong>Certified By:</strong><br/>
<t t-if="signature_img">
<img t-att-src="'data:image/png;base64,%s' % signature_img.decode()"
style="max-height: 22mm; max-width: 70mm;"/>
</t><br/>
<strong>Name:</strong> <span t-esc="signer_name"/>
</td>
<td style="width: 50%; vertical-align: top;">
<strong>Certification Statement:</strong>
<span style="font-size: 8.5pt;">
Ref. WO# <span t-esc="job and job.name or ''"/>
</span>
<p style="font-size: 8pt; margin-top: 4px;">
We certify that the parts listed above have been processed in
accordance with the specifications referenced and that all
required tests have been performed. Records on file at our
facility per AS9100 / ISO 9001 retention policy.
</p>
</td>
</tr>
</table>
</div>
</template>
<!-- ============================================================== -->
<!-- Wrapper that picks chronological vs classic body -->
<!-- ============================================================== -->
<template id="coc_body_router">
<t t-if="doc.body_style == 'chronological' and 'x_fc_job_id' in doc._fields and doc.x_fc_job_id">
<t t-call="fusion_plating_reports.coc_body_chronological"/>
</t>
<t t-else="">
<t t-call="fusion_plating_reports.coc_body"/>
</t>
</template>
</odoo>
- Step 4: Wire the router into the existing CoC actions
In fusion_plating_reports/report/report_coc.xml, find the templates that render coc_body (search for t-call="fusion_plating_reports.coc_body") and replace with t-call="fusion_plating_reports.coc_body_router". There should be ≤4 occurrences (en + fr × portrait + landscape).
If the router replacement breaks anything, revert to direct calls and gate per-template instead.
- Step 5: Commit
git add fusion_plating_reports/report/report_coc_chronological.xml \
fusion_plating_reports/report/report_coc.xml \
fusion_plating_certificates/models/fp_certificate.py \
fusion_plating_certificates/views/fp_certificate_views.xml
git commit -m "feat(sub12c): chronological CoC body + body_style opt-in (Task 3)
New template: fusion_plating_reports.coc_body_chronological.
Walks fp.job.step.move records in time order (chain-of-custody view).
Per-move heading 'Step Name (Tank Code)' with 'Moved By / Time / Qty'
meta line + a 5-column measurement sub-table (Name / Description /
Target / Actual / Recorded By) when the destination step has captured
inputs. Heading-only when there are no inputs (gating moves).
New router template: coc_body_router. Picks chronological vs classic
based on fp.certificate.body_style. Existing certs default to 'classic'
so no regressions.
fp.certificate.body_style ('classic' | 'chronological') exposed on the
form. Customer chooses per cert.
Sign-off block reuses the existing owner_user_id signature pattern +
x_fc_coc_signature_override fallback. Cert statement boilerplate is
inline (Sub 12d will move it to a configurable per-customer field).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
Task 4: Labor History views
Files:
-
Create:
fusion_plating/views/fp_job_step_timelog_views.xml -
Step 1: Create the views file
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12c — Labor History views.
fp.job.step.timelog now has a state machine + reconciliation
columns (Sub 12b). This file surfaces the history under
Plating → Operations → Labor History for billing audit + payroll
reconciliation.
-->
<odoo>
<record id="view_fp_job_step_timelog_list" model="ir.ui.view">
<field name="name">fp.job.step.timelog.list</field>
<field name="model">fp.job.step.timelog</field>
<field name="arch" type="xml">
<list string="Labor History" default_order="date_started desc"
decoration-info="state == 'running'"
decoration-warning="state == 'paused'"
decoration-muted="state == 'reconciled'">
<field name="user_id"/>
<field name="job_id"/>
<field name="step_id"/>
<field name="state" widget="badge"
decoration-info="state == 'running'"
decoration-warning="state == 'paused'"
decoration-success="state == 'stopped'"
decoration-muted="state == 'reconciled'"/>
<field name="date_started"/>
<field name="date_finished" optional="show"/>
<field name="accrued_seconds" optional="show"/>
<field name="billed_hrs" optional="show"/>
<field name="billed_min" optional="show"/>
<field name="billed_sec" optional="show"/>
<field name="billed_pct" widget="progressbar" optional="show"/>
<field name="product_id" optional="hide"/>
</list>
</field>
</record>
<record id="view_fp_job_step_timelog_form" model="ir.ui.view">
<field name="name">fp.job.step.timelog.form</field>
<field name="model">fp.job.step.timelog</field>
<field name="arch" type="xml">
<form string="Labor Timer" create="false">
<header>
<field name="state" widget="statusbar"
statusbar_visible="running,paused,stopped,reconciled"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="display_name" readonly="1"/></h1>
</div>
<group>
<group>
<field name="user_id" readonly="1"/>
<field name="job_id" readonly="1"/>
<field name="step_id" readonly="1"/>
<field name="date_started" readonly="1"/>
<field name="date_finished" readonly="1"/>
</group>
<group>
<field name="accrued_seconds" readonly="1"/>
<label for="billed_hrs" string="Billed Time"/>
<div>
<field name="billed_hrs" class="oe_inline"
readonly="state in ('reconciled',)"
groups="fusion_plating.group_fusion_plating_supervisor"/>
hrs
<field name="billed_min" class="oe_inline"
readonly="state in ('reconciled',)"
groups="fusion_plating.group_fusion_plating_supervisor"/>
min
<field name="billed_sec" class="oe_inline"
readonly="state in ('reconciled',)"
groups="fusion_plating.group_fusion_plating_supervisor"/>
sec
</div>
<field name="billed_pct" widget="progressbar" readonly="1"/>
<field name="product_id"/>
</group>
</group>
<group string="Notes">
<field name="notes" nolabel="1"/>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_job_step_timelog_search" model="ir.ui.view">
<field name="name">fp.job.step.timelog.search</field>
<field name="model">fp.job.step.timelog</field>
<field name="arch" type="xml">
<search>
<field name="user_id"/>
<field name="job_id"/>
<field name="step_id"/>
<field name="product_id"/>
<separator/>
<filter string="My Timers" name="my_timers"
domain="[('user_id','=',uid)]"/>
<filter string="Today" name="today"
domain="[('date_started','>=',(context_today() ).strftime('%Y-%m-%d 00:00:00'))]"/>
<filter string="This Week" name="this_week"
domain="[('date_started','>=',(context_today() - relativedelta(days=context_today().weekday())).strftime('%Y-%m-%d 00:00:00'))]"/>
<separator/>
<filter string="Running" name="running"
domain="[('state','=','running')]"/>
<filter string="Paused" name="paused"
domain="[('state','=','paused')]"/>
<filter string="Pending Reconciliation" name="pending"
domain="[('state','=','stopped')]"/>
<filter string="Reconciled" name="reconciled"
domain="[('state','=','reconciled')]"/>
<group>
<filter string="Operator" name="group_user"
context="{'group_by':'user_id'}"/>
<filter string="Job" name="group_job"
context="{'group_by':'job_id'}"/>
<filter string="Date" name="group_date"
context="{'group_by':'date_started:day'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_labor_history" model="ir.actions.act_window">
<field name="name">Labor History</field>
<field name="res_model">fp.job.step.timelog</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_job_step_timelog_search"/>
<field name="context">{'search_default_my_timers': 1}</field>
</record>
<menuitem id="menu_fp_labor_history"
name="Labor History"
parent="menu_fp_root"
action="action_fp_labor_history"
sequence="64"/>
</odoo>
- Step 2: Add ACL rows for the timelog model
The model is already accessible via fp.job.step relations, but explicit rows make the menu work for non-admin users. Append to fusion_plating/security/ir.model.access.csv:
access_fp_job_step_timelog_operator,fp.job.step.timelog.operator,model_fp_job_step_timelog,group_fusion_plating_operator,1,1,0,0
access_fp_job_step_timelog_supervisor,fp.job.step.timelog.supervisor,model_fp_job_step_timelog,group_fusion_plating_supervisor,1,1,1,0
access_fp_job_step_timelog_manager,fp.job.step.timelog.manager,model_fp_job_step_timelog,group_fusion_plating_manager,1,1,1,1
(Skip if already present — grep first: grep model_fp_job_step_timelog fusion_plating/security/ir.model.access.csv.)
- Step 3: Commit
git add fusion_plating/views/fp_job_step_timelog_views.xml \
fusion_plating/security/ir.model.access.csv
git commit -m "feat(sub12c): Labor History views (Task 4)
Plating → Operations → Labor History (sequence 64, between Move Log
62 and Aerospace 65). List view colour-coded by state (info/warning/
success/muted), with billed_pct progressbar.
Search filters: My Timers (default), Today, This Week, Running,
Paused, Pending Reconciliation, Reconciled. Group-by: Operator, Job,
Date.
Form view (read-only header with statusbar): identity fields readonly,
billed_hrs/min/sec editable for supervisors+ until state=reconciled,
chatter for operator notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
Task 5: Deploy to entech + smoke test + push
Files:
-
(none — deployment + manual verification)
-
Step 1: Tar + ship
tar -cf - \
fusion_plating/__manifest__.py \
fusion_plating/security/ir.model.access.csv \
fusion_plating/views/fp_job_step_timelog_views.xml \
fusion_plating_jobs/__manifest__.py \
fusion_plating_jobs/report/report_fp_job_traveller.xml \
fusion_plating_reports/__manifest__.py \
fusion_plating_reports/report/report_coc.xml \
fusion_plating_reports/report/report_coc_chronological.xml \
fusion_plating_certificates/models/fp_certificate.py \
fusion_plating_certificates/views/fp_certificate_views.xml \
| ssh pve-worker5 "pct exec 111 -- bash -c 'cd /mnt/extra-addons/custom && tar -xf -'"
- Step 2: Update modules
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && \
su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin \
-u fusion_plating,fusion_plating_jobs,fusion_plating_reports,fusion_plating_certificates --stop-after-init\" 2>&1 | tail -25 && \
systemctl start odoo'"
Expected: clean upgrade, 233 modules loaded.
- Step 3: Clear asset cache
ssh pve-worker5 "pct exec 111 -- bash -c \"su - postgres -c 'psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\\''/web/assets/%'\\'';\\\"'\""
- Step 4: Manual smoke test
- Open any in-flight
fp.job→ Print → "Job Traveller". PDF should render in A4 landscape with: header (logo + barcode + dates + customer), Item Information block, Process-Sheet header, Routing table with target columns + blank actuals. - Open any
fp.certificate→ form shows new "CoC Body Style" Selection. Default = Classic. Existing CoC PDF unchanged. - Flip body_style to Chronological → Print CoC → new PDF walks moves in time order with measurement tables. (Job needs
fp.job.step.moverows for this to be meaningful — produce a few via the Sub 12b tablet flow first if needed.) - Plating → Operations → Labor History menu appears. List shows timelog rows with My Timers default filter. Try filters (Running / Paused / Pending Reconciliation / Reconciled) and Group-by (Operator / Job / Date).
- Open a
reconciledtimelog → form is read-only, supervisor can re-edit billed_* if needed.
- Step 5: Push to remote
git push origin main
Self-Review
Spec coverage check
| Spec section 6 item | Task |
|---|---|
| 6.2 Operator Traveller v2 (A4 landscape, paper-style) | Task 2 |
| 6.3 Customer CoC chronological body | Task 3 |
| 6.3 body_style opt-in field | Task 3 |
| 6.4 Labor History list/form/search/group-by/menu | Task 4 |
| 6.4 Manager re-edit of billed_* on reconciled | Task 4 (form view + supervisor group on billed_* fields) |
| 6.5 Backend support (chronological payload helper) | Inline in Task 3 — QWeb walks job.move_ids.sorted('move_datetime') directly; no separate Python helper needed |
| 6.6 Migration / install | Task 1 (version bumps) — no model migrations, all additive |
| 6.7 Verification | Task 5 |
| 6.8 Things to NOT do | Honoured — report_coc.xml legacy bodies untouched, action_issue flow not changed, no new model fields beyond body_style, two reports stay separate |
Out-of-scope items handled by deferring:
- Rack travel ticket PDF (Sub 12b's Save+Print 404) — flagged in plan companion docs as a follow-up
- Per-customer cert statement — boilerplate inline in chronological body for now; deferrable
Placeholder scan
No "TBD" / "TODO" / "implement later" / "fill in details".
The chronological body's measurement sub-table renders prompts + targets but leaves the Actual column blank. That's because Sub 12a + Sub 12b's runtime captures step_input values via the operator's per-step input form, which lands in the existing step.input_value_ids collection (or equivalent) — wiring that into the Actual cell needs more knowledge of the existing input-value model than the plan time budget allows. Documented in Task 3's commit message as a Sub 12d follow-up.
Type / signature consistency
fp.certificate.body_styledefined Task 3, used bycoc_body_routerTask 3. ✓coc_body_chronologicaltemplate defined Task 3, called bycoc_body_routerTask 3. ✓coc_body_routertemplate defined Task 3, called from existingreport_coc.xmltemplates after the replacement edit (Task 3 step 4). ✓fp.job.move_ids(added by Sub 12b Task 6) referenced by Task 3's chronological body. ✓fp.job.step.timelog.state+accrued_seconds+billed_*+product_id(added by Sub 12b Task 7) referenced by Task 4's views. ✓paperformat_fp_traveller_landscapedefined Task 2, referenced byaction_report_fp_job_travellerTask 2 same record. ✓
Plan complete. 5 tasks, ~1 day end-to-end (significantly tighter than original 18-task plan because most CoC infrastructure already exists in fusion_plating_reports).