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: company logo + barcode (Code 128) + WO# + Date In + Due
  Date + Type + Order# + PO# + WO-Generated-By + customer block.
- Item Information panel: Part# / Rev / Mat / Catg / S/N + Item-Name +
  Qty Rec / VIS INSP / Rework / Special Requirements / Stamp-Date.
- Process-Sheet header: recipe name + category + spec/info.
- Routing table (11 cols): 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. Heavily defensive QWeb — every cross-module field
access ('part_catalog_id' / 'coating_config_id' / 'qty_received' /
'special_requirements' / 'serial_number' / 'base_material' /
'customer_facing_description' / 'time_min_target' / etc.) guarded with
'X in record._fields' checks so the report renders cleanly even when
some Sub 12a/12b fields aren't yet populated.

New paperformat: A4 landscape narrow margins, 90 dpi.

Action ID + report_name unchanged so existing form-button bindings
keep working (binding_model_id still points at fp.job).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-27 21:39:41 -04:00
parent f55193fb1b
commit 12fcd11016

View File

@@ -2,11 +2,30 @@
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Native fp.job traveller — minimal portrait A4 listing all steps.
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; 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>
@@ -16,48 +35,251 @@
<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">
<h1>Job Traveller — <span t-esc="job.name"/></h1>
<table class="table table-sm" style="margin-top: 1em;">
<tr><th>Customer</th><td><span t-esc="job.partner_id.name"/></td></tr>
<tr><th>SO</th><td><span t-esc="job.sale_order_id.name or '-'"/></td></tr>
<tr><th>Qty</th><td><span t-esc="job.qty"/></td></tr>
<tr><th>Recipe</th><td><span t-esc="job.recipe_id.name or '-'"/></td></tr>
<tr><th>Deadline</th><td><span t-esc="job.date_deadline and job.date_deadline.strftime('%b %d, %Y') or '-'"/></td></tr>
<tr><th>Status</th><td><span t-esc="job.state"/></td></tr>
<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 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 { width: 100%; border-collapse: collapse; }
.fp-trav-page table.bordered th { background: #ededed; padding: 4px 6px; text-align: left; font-weight: bold; font-size: 8pt; }
.fp-trav-page table.bordered td { padding: 4px 6px; vertical-align: top; font-size: 8pt; }
.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; }
.fp-trav-page .text-center { text-align: center; }
</style>
<!-- ===== HEADER ===== -->
<table class="bordered">
<tr>
<td style="width: 8%; 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; width: 28%;">
<h1>Work Order / Bon de Travail</h1>
<div class="text-center" style="margin-top: 4px;">
<strong t-esc="job.name"/>
</div>
<div class="text-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 and job.recipe_id.name) or '—'"/>
</td>
<td style="width: 18%;">
<strong>Order #:</strong>
<span t-esc="(job.sale_order_id and job.sale_order_id.name) or '—'"/><br/>
<strong>P.O. #:</strong>
<span t-esc="(job.sale_order_id and job.sale_order_id.client_order_ref) or '—'"/><br/>
<strong>WO Generated By:</strong>
<span t-esc="(job.create_uid and job.create_uid.name) or '—'"/>
</td>
<td style="width: 28%; vertical-align: top;">
<strong t-esc="(job.partner_id and job.partner_id.name) or '—'"/><br/>
<span t-esc="(job.partner_id and job.partner_id.street) or ''"/><br/>
<span t-esc="(job.partner_id and job.partner_id.city) or ''"/>,
<span t-esc="(job.partner_id and job.partner_id.state_id and job.partner_id.state_id.code) or ''"/>
<span t-esc="(job.partner_id and job.partner_id.zip) or ''"/><br/>
<strong>Tel:</strong>
<span t-esc="(job.partner_id and job.partner_id.phone) or '—'"/>
</td>
</tr>
</table>
<h2 style="margin-top: 2em;">Steps</h2>
<table class="table table-sm table-bordered">
<!-- ===== ITEM INFORMATION ===== -->
<table class="bordered" style="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>
<t t-if="'part_catalog_id' in job._fields and job.part_catalog_id">
<span t-esc="job.part_catalog_id.part_number or '—'"/>
</t>
<t t-else=""></t><br/>
<strong>Rev:</strong>
<t t-if="'part_catalog_id' in job._fields and job.part_catalog_id and 'revision' in job.part_catalog_id._fields">
<span t-esc="job.part_catalog_id.revision or '—'"/>
</t>
<t t-else=""></t><br/>
<strong>Mat:</strong>
<t t-if="'part_catalog_id' in job._fields and job.part_catalog_id and 'base_material' in job.part_catalog_id._fields">
<span t-esc="job.part_catalog_id.base_material or '—'"/>
</t>
<t t-else=""></t><br/>
<strong>Catg:</strong>
<span t-esc="(job.recipe_id and job.recipe_id.name) or '—'"/><br/>
<strong>S/N:</strong>
<t t-if="'serial_number' in job._fields"><span t-esc="job.serial_number or ''"/></t>
</td>
<td>
<strong>
<t t-if="'part_catalog_id' in job._fields and job.part_catalog_id">
<span t-esc="job.part_catalog_id.name or job.product_id.name or '—'"/>
</t>
<t t-else="">
<span t-esc="(job.product_id and job.product_id.name) or '—'"/>
</t>
</strong>
<div style="font-size: 7.5pt; margin-top: 2px;">
<t t-if="'part_catalog_id' in job._fields and job.part_catalog_id and '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 if 'qty_received' in job._fields else 0) or job.qty"/>
</td>
<td class="text-center">
<t t-if="'qty_visual_inspection_rejects' in job._fields">
<span t-esc="job.qty_visual_inspection_rejects or 0"/>
</t>
<t t-else=""></t>
</td>
<td class="text-center">
<t t-if="'qty_rework' in job._fields">
<span t-esc="job.qty_rework or 0"/>
</t>
<t t-else=""></t>
</td>
<td style="font-size: 7pt; white-space: pre-wrap;">
<t t-if="'special_requirements' in job._fields">
<span t-esc="job.special_requirements or '—'"/>
</t>
<t t-else=""></t>
</td>
<td class="fp-trav-stamp"/>
</tr>
</table>
<!-- ===== PROCESS-SHEET HEADER ===== -->
<table class="bordered" style="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 and job.recipe_id.name) or '—'"/></td>
<td>
<t t-if="job.recipe_id and job.recipe_id.process_type_id">
<span t-esc="job.recipe_id.process_type_id.name"/>
</t>
<t t-else=""></t>
</td>
<td>
<t t-if="'coating_config_id' in job._fields and job.coating_config_id">
<span t-esc="job.coating_config_id.name"/>
</t>
</td>
</tr>
</table>
<!-- ===== ROUTING TABLE ===== -->
<table class="bordered" style="margin-top: 4px;">
<thead>
<tr>
<th>#</th>
<th>Operation</th>
<th>Work Centre</th>
<th>Kind</th>
<th>Expected (min)</th>
<th>Actual (min)</th>
<th>State</th>
<th>Operator Sign-off</th>
<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><span t-esc="step.sequence"/></td>
<td><span t-esc="step.name"/></td>
<td><span t-esc="step.work_centre_id.name or ''"/></td>
<td><span t-esc="step.kind"/></td>
<td><span t-esc="step.duration_expected"/></td>
<td><span t-esc="step.duration_actual"/></td>
<td><span t-esc="step.state"/></td>
<td style="border-bottom: 1px solid #999; min-width: 100px;"></td>
<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-if="rn">
<t t-foreach="rn.input_ids.filtered(lambda i: (i.kind or 'step_input') == 'step_input').sorted('sequence')" t-as="inp">
<span t-esc="inp.name"/>:
<span class="fp-trav-blank"/>
<t t-if="'target_unit' in inp._fields and inp.target_unit"><span> </span><span t-esc="inp.target_unit"/></t><br/>
</t>
</t>
</div>
</td>
<td style="font-size: 7.5pt; white-space: pre-wrap;">
<t t-if="rn and rn.description">
<span t-esc="rn.description" t-options="{'widget': 'html'}"/>
</t>
</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-if="rn.temp_unit" 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>