Files
Odoo-Modules/fusion_plating/docs/superpowers/plans/2026-04-28-sub12c-reports-labor-history.md
gsinghpal 34528a5d3d docs(sub12c): implementation plan — 5 tasks (down from original 18)
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>
2026-04-27 21:36:06 -04:00

48 KiB
Raw Blame History

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:

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.xmlcoc_body template + report_coc_en / report_coc_fr actions. 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-init clean 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_style field on fp.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_style on 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"/>
                    &nbsp;·&nbsp;
                    <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">
                        &nbsp;·&nbsp;<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','&gt;=',(context_today() ).strftime('%Y-%m-%d 00:00:00'))]"/>
                <filter string="This Week" name="this_week"
                        domain="[('date_started','&gt;=',(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
  1. 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.
  2. Open any fp.certificate → form shows new "CoC Body Style" Selection. Default = Classic. Existing CoC PDF unchanged.
  3. Flip body_style to Chronological → Print CoC → new PDF walks moves in time order with measurement tables. (Job needs fp.job.step.move rows for this to be meaningful — produce a few via the Sub 12b tablet flow first if needed.)
  4. 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).
  5. Open a reconciled timelog → 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_style defined Task 3, used by coc_body_router Task 3. ✓
  • coc_body_chronological template defined Task 3, called by coc_body_router Task 3. ✓
  • coc_body_router template defined Task 3, called from existing report_coc.xml templates 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_landscape defined Task 2, referenced by action_report_fp_job_traveller Task 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).