fix(reports): restore the original ENTECH box-sticker layout for fp.job + sale.order

The original mrp.production / mrp.workorder sticker (logo + WO# stack
on the left, big QR on the right, 7-row body with PO/Customer/Process/
Part Number/Due/Qty/Notes — the design ENTECH has been printing for
months) lives in fusion_plating_reports.report_fp_wo_sticker_inner.

The new fp.job sticker had been rebuilt from scratch with a different
look. This wires fp.job into the existing canonical template instead.

What changed:

- report_fp_wo_sticker_inner — every t-set now uses the
  "_var or fallback-from-_mo" pattern so callers can pre-resolve
  values; mrp.production/mrp.workorder callers still work via the
  fallback path.
- report_fp_wo_sticker_defaults — new shared template that initialises
  every overridable name to False so the inner's `or` chain doesn't
  NameError when an outer hasn't set it.
- report_fp_job_sticker_template — replaces the parallel layout with
  a t-call to report_fp_wo_sticker_inner, feeding it from fp.job
  fields (name, partner_id, qty, date_deadline, sale_order_id,
  sale_order_line_ids, recipe_id, part_catalog_id, coating_config_id).
- report_fp_so_sticker — new outer that iterates sale.order.order_line
  and emits one sticker per line that has a part_catalog_id. Bound to
  sale.order's print menu via action_report_fp_so_sticker.

Versions: reports 19.0.7.14.0 -> 19.0.7.15.0,
          jobs    19.0.5.0.0  -> 19.0.5.1.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-25 13:03:29 -04:00
parent c27e8a109c
commit ecac43eef4
5 changed files with 159 additions and 97 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
{ {
'name': 'Fusion Plating — Native Jobs', 'name': 'Fusion Plating — Native Jobs',
'version': '19.0.5.0.0', 'version': '19.0.5.1.0',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.', 'author': 'Nexa Systems Inc.',

View File

@@ -3,12 +3,12 @@
Copyright 2026 Nexa Systems Inc. Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0) License OPL-1 (Odoo Proprietary License v1.0)
Native fp.job sticker — parallel to fusion_plating_reports' WO Box Native fp.job sticker — reuses the canonical box-sticker design from
Sticker which binds to mrp.production/mrp.workorder. Coexists during fusion_plating_reports.report_fp_wo_sticker_inner. The visual layout
the migration period. (logo + WO# stack on the left, big QR on the right, 7-row body table
underneath, all wrapped in a 2px border) is the one shop staff have
QR encodes /fp/job/<id> (controller added in Task TBD; for now scan been printing since the mrp.production days; we just feed it from
will fall back to /web/login if controller absent). fp.job fields here instead of mrp.production.
--> -->
<odoo> <odoo>
@@ -43,73 +43,29 @@
<template id="report_fp_job_sticker_template"> <template id="report_fp_job_sticker_template">
<t t-call="web.html_container"> <t t-call="web.html_container">
<t t-foreach="docs" t-as="job"> <t t-foreach="docs" t-as="job">
<t t-set="_base_url" t-value="env['ir.config_parameter'].sudo().get_param('web.base.url', '')"/> <!-- Defaults block initialises every var the inner
<t t-set="_scan_url" t-value="_base_url + '/fp/job/' + str(job.id)"/> reads (so `_so or ...` doesn't NameError). We
<t t-set="_qr_src" t-value="env['ir.actions.report'].barcode_data_uri('QR', _scan_url, width=300, height=300)"/> then override the ones we have data for. -->
<style> <t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
@page { margin: 0; size: 152mm 102mm; } <!-- Pre-resolve the variables the shared inner template
html, body { margin: 0 !important; padding: 0 !important; width: 100% !important; height: 100% !important; } expects, sourcing them from fp.job's native fields. -->
.fp-job-sticker { <t t-set="_order_id" t-value="job.name"/>
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; <t t-set="_scan_id" t-value="job.id"/>
color: #000; <t t-set="_scan_path" t-value="'/fp/job/'"/>
position: absolute; <t t-set="_mo" t-value="False"/>
top: 4px; left: 4px; right: 4px; bottom: 4px; <t t-set="_so" t-value="job.sale_order_id"/>
padding: 8px; <t t-set="_line" t-value="job.sale_order_line_ids[:1]"/>
box-sizing: border-box; <t t-set="_part" t-value="('part_catalog_id' in job._fields and job.part_catalog_id) or False"/>
border: 2px solid #000; <t t-set="_coating" t-value="('coating_config_id' in job._fields and job.coating_config_id) or False"/>
} <t t-set="_process" t-value="job.recipe_id or False"/>
.fp-job-sticker-head { <t t-set="_due" t-value="job.date_deadline or False"/>
display: flex; <t t-set="_qty" t-value="job.qty"/>
justify-content: space-between; <t t-set="_partner_name" t-value="job.partner_id.name"/>
align-items: flex-start; <!-- The fp.job's own name (WH/JOB/00033) is already
border-bottom: 1.5px solid #000; printed in the header as "WO #...", so suppress
padding-bottom: 6px; the muted "(WH/MO/...)" suffix on the PO row. -->
margin-bottom: 8px; <t t-set="_mo_ref" t-value="''"/>
} <t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
.fp-job-sticker-id {
font-size: 36pt;
font-weight: 900;
line-height: 1;
}
.fp-job-sticker-qr { width: 32mm; height: 32mm; display: block; }
.fp-job-sticker-row { padding: 3px 0; font-size: 14pt; }
.fp-job-sticker-label { font-weight: 700; display: inline-block; min-width: 30mm; }
</style>
<div class="fp-job-sticker">
<div class="fp-job-sticker-head">
<div>
<div style="font-size: 10pt; letter-spacing: 0.5mm; text-transform: uppercase;">Plating Job</div>
<div class="fp-job-sticker-id">
<span t-esc="job.name"/>
</div>
</div>
<img class="fp-job-sticker-qr" t-if="_qr_src" t-att-src="_qr_src"/>
</div>
<div class="fp-job-sticker-row">
<span class="fp-job-sticker-label">Customer:</span>
<span t-esc="job.partner_id.name"/>
</div>
<div class="fp-job-sticker-row" t-if="job.sale_order_id">
<span class="fp-job-sticker-label">SO:</span>
<span t-esc="job.sale_order_id.name"/>
</div>
<div class="fp-job-sticker-row">
<span class="fp-job-sticker-label">Qty:</span>
<strong><span t-esc="int(job.qty) if job.qty == int(job.qty) else job.qty"/></strong>
</div>
<div class="fp-job-sticker-row" t-if="job.date_deadline">
<span class="fp-job-sticker-label">Due:</span>
<span t-esc="job.date_deadline.strftime('%b %d, %Y')"/>
</div>
<div class="fp-job-sticker-row" t-if="job.recipe_id">
<span class="fp-job-sticker-label">Recipe:</span>
<span t-esc="job.recipe_id.name"/>
</div>
<div class="fp-job-sticker-row">
<span class="fp-job-sticker-label">Steps:</span>
<span t-esc="job.step_done_count"/> / <span t-esc="job.step_count"/>
</div>
</div>
</t> </t>
</t> </t>
</template> </template>

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
{ {
'name': 'Fusion Plating — Reports', 'name': 'Fusion Plating — Reports',
'version': '19.0.7.14.0', 'version': '19.0.7.15.0',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.', 'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
'depends': [ 'depends': [

View File

@@ -369,6 +369,24 @@
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/> <field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
</record> </record>
<!-- Same sticker bound to sale.order — prints one sticker per
order line that carries a part, so estimators / receiving can
hand them to the floor before fp.jobs even exist. Uses the
same paperformat (6x4") so estimators don't need to think
about page size; the output PDF is multi-page if the SO has
multiple plating lines. -->
<record id="action_report_fp_so_sticker" model="ir.actions.report">
<field name="name">WO Box Sticker</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_so_sticker</field>
<field name="report_file">fusion_plating_reports.report_fp_so_sticker</field>
<field name="print_report_name">'WO Sticker - %s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
</record>
<!-- ============================================================= --> <!-- ============================================================= -->
<!-- 15. Packing Slip (Portrait + Landscape) --> <!-- 15. Packing Slip (Portrait + Landscape) -->
<!-- ============================================================= --> <!-- ============================================================= -->

View File

@@ -5,38 +5,70 @@
Parts-box identification sticker — printed on a 4x3" label. Parts-box identification sticker — printed on a 4x3" label.
Bound to BOTH mrp.production (MO) and mrp.workorder (WO) because Bound to mrp.production (MO), mrp.workorder (WO), fp.job, and
the shop talks in "WO #" terms (Steelhead legacy) but the data sale.order. The shop talks in "WO #" terms (Steelhead legacy) but
hangs off the MO record. The inner template normalises either the data may hang off any of those records. The inner template
input to the same set of resolved variables: normalises every input to the same set of resolved variables and
accepts either pre-resolved values from the outer template OR
resolves them itself from `_mo` when called from an mrp.* context.
Variables an outer template MAY pre-set (otherwise falls back to
`_mo`-based resolution):
* _order_id — number to print as "WO #" * _order_id — number to print as "WO #"
* _mo — the mrp.production record * _scan_id — id encoded into the QR URL
* _scan_path — '/fp/job/' or '/fp/wo/' prefix (default '/fp/wo/')
* _mo — the mrp.production record (or False)
* _so, _line — the originating sale order / line * _so, _line — the originating sale order / line
* _part — fp.part.catalog * _part — fp.part.catalog
* _coating — fp.coating.config * _coating — fp.coating.config
* _process — the resolved fusion.plating.process.node tree * _process — the resolved fusion.plating.process.node tree
* _scan_url — base_url + /fp/wo/<id> (encoded into the QR) * _due — datetime/date for "Due Date" row
* _qty — float for "Qty" row
* _po_number — overrides _so.x_fc_po_number
* _partner_name — overrides _so.partner_id.name
* _mo_ref — string shown muted in "(WH/MO/...)" — '' to hide
* _internal_note— free text for "Notes" row
--> -->
<odoo> <odoo>
<!-- ========== Shared inner template ========== --> <!-- ========== Shared inner template ========== -->
<template id="report_fp_wo_sticker_inner"> <template id="report_fp_wo_sticker_inner">
<t t-set="_base_url" t-value="env['ir.config_parameter'].sudo().get_param('web.base.url', '')"/> <t t-set="_base_url" t-value="env['ir.config_parameter'].sudo().get_param('web.base.url', '')"/>
<t t-set="_scan_url" t-value="_base_url + '/fp/wo/' + str(_scan_id)"/> <t t-set="_scan_path" t-value="_scan_path or '/fp/wo/'"/>
<t t-set="_so" t-value="_mo and env['sale.order'].sudo().search( <t t-set="_scan_url" t-value="_base_url + _scan_path + str(_scan_id)"/>
[('name', '=', _mo.origin)], limit=1) or False"/> <!-- Each variable: prefer the outer-supplied value, otherwise
<t t-set="_line" t-value="(_mo and 'x_fc_sale_order_line_ids' in _mo._fields resolve from _mo. This lets fp.job / sale.order outers feed
and _mo.x_fc_sale_order_line_ids[:1]) pre-resolved data while keeping the original mrp.production /
mrp.workorder callers working untouched. -->
<t t-set="_so" t-value="_so or (_mo and env['sale.order'].sudo().search(
[('name', '=', _mo.origin)], limit=1)) or False"/>
<t t-set="_line" t-value="_line
or (_mo and 'x_fc_sale_order_line_ids' in _mo._fields
and _mo.x_fc_sale_order_line_ids[:1])
or (_so and _so.order_line[:1]) or (_so and _so.order_line[:1])
or False"/> or False"/>
<t t-set="_part" t-value="_line and _line.x_fc_part_catalog_id or False"/> <t t-set="_part" t-value="_part or (_line and _line.x_fc_part_catalog_id) or False"/>
<t t-set="_coating" t-value="_line and _line.x_fc_coating_config_id or False"/> <t t-set="_coating" t-value="_coating or (_line and _line.x_fc_coating_config_id) or False"/>
<t t-set="_process" t-value="(_part and _part.default_process_id) <t t-set="_process" t-value="_process
or (_part and _part.default_process_id)
or (_coating and _coating.recipe_id) or (_coating and _coating.recipe_id)
or False"/> or False"/>
<t t-set="_due" t-value="(_mo and (_mo.date_deadline or _mo.date_finished)) <t t-set="_due" t-value="_due
or (_mo and (_mo.date_deadline or _mo.date_finished))
or (_line and _line.x_fc_part_deadline) or (_line and _line.x_fc_part_deadline)
or False"/> or False"/>
<t t-set="_qty" t-value="_qty if _qty is not None and _qty is not False
else (_mo and _mo.product_qty) or 0"/>
<t t-set="_po_number" t-value="_po_number or (_so and _so.x_fc_po_number) or '-'"/>
<t t-set="_partner_name" t-value="_partner_name or (_so and _so.partner_id.name) or '-'"/>
<!-- _mo_ref controls the muted "(WH/MO/00033)" suffix next to PO.
Outer can pass '' to hide it (e.g. fp.job already shows its
own name in the header). Defaults to _mo.name. -->
<t t-set="_mo_ref" t-value="_mo_ref if _mo_ref is not None and _mo_ref is not False
else (_mo and _mo.name) or ''"/>
<t t-set="_internal_note" t-value="_internal_note
or (_so and _so.x_fc_internal_note
and _so.x_fc_internal_note.striptags()[:100])
or '-'"/>
<!-- Inline the QR as base64 data URI so wkhtmltopdf doesn't need <!-- Inline the QR as base64 data URI so wkhtmltopdf doesn't need
to fetch /report/barcode/ over the network during rendering. --> to fetch /report/barcode/ over the network during rendering. -->
<t t-set="_qr_src" t-value="env['ir.actions.report'].barcode_data_uri( <t t-set="_qr_src" t-value="env['ir.actions.report'].barcode_data_uri(
@@ -241,10 +273,10 @@
<td class="fp-sticker-label">PO (RO):</td> <td class="fp-sticker-label">PO (RO):</td>
<td class="fp-sticker-value"> <td class="fp-sticker-value">
<span class="fp-sticker-strong" <span class="fp-sticker-strong"
t-esc="(_so and _so.x_fc_po_number) or '-'"/> t-esc="_po_number"/>
<t t-if="_mo"> <t t-if="_mo_ref">
<span class="fp-sticker-muted"> <span class="fp-sticker-muted">
(<span t-esc="_mo.name"/>) (<span t-esc="_mo_ref"/>)
</span> </span>
</t> </t>
</td> </td>
@@ -252,7 +284,7 @@
<tr> <tr>
<td class="fp-sticker-label">Customer:</td> <td class="fp-sticker-label">Customer:</td>
<td class="fp-sticker-value"> <td class="fp-sticker-value">
<span t-esc="(_so and _so.partner_id.name) or '-'"/> <span t-esc="_partner_name"/>
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -295,7 +327,6 @@
<td class="fp-sticker-label">Qty:</td> <td class="fp-sticker-label">Qty:</td>
<td class="fp-sticker-value"> <td class="fp-sticker-value">
<span class="fp-sticker-strong"> <span class="fp-sticker-strong">
<t t-set="_qty" t-value="_mo and _mo.product_qty or 0"/>
<span t-esc="int(_qty) if _qty == int(_qty) else _qty"/> <span t-esc="int(_qty) if _qty == int(_qty) else _qty"/>
</span> </span>
</td> </td>
@@ -303,8 +334,7 @@
<tr> <tr>
<td class="fp-sticker-label">Notes:</td> <td class="fp-sticker-label">Notes:</td>
<td class="fp-sticker-value"> <td class="fp-sticker-value">
<t t-esc="(_so and _so.x_fc_internal_note <t t-esc="_internal_note"/>
and _so.x_fc_internal_note.striptags()[:100]) or '-'"/>
</td> </td>
</tr> </tr>
</table> </table>
@@ -312,10 +342,32 @@
</div> </div>
</template> </template>
<!-- =====================================================
Reusable defaults block — every outer template t-calls
this BEFORE the sticker inner so `_so`, `_line`, etc.
are always defined. The inner's `_so or fallback`
pattern relies on these names existing in scope.
===================================================== -->
<template id="report_fp_wo_sticker_defaults">
<t t-set="_so" t-value="False"/>
<t t-set="_line" t-value="False"/>
<t t-set="_part" t-value="False"/>
<t t-set="_coating" t-value="False"/>
<t t-set="_process" t-value="False"/>
<t t-set="_due" t-value="False"/>
<t t-set="_qty" t-value="False"/>
<t t-set="_po_number" t-value="False"/>
<t t-set="_partner_name" t-value="False"/>
<t t-set="_mo_ref" t-value="False"/>
<t t-set="_internal_note" t-value="False"/>
<t t-set="_scan_path" t-value="False"/>
</template>
<!-- ========== Outer template — mrp.workorder entry ========== --> <!-- ========== Outer template — mrp.workorder entry ========== -->
<template id="report_fp_wo_sticker"> <template id="report_fp_wo_sticker">
<t t-call="web.html_container"> <t t-call="web.html_container">
<t t-foreach="docs" t-as="doc"> <t t-foreach="docs" t-as="doc">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="doc.id"/> <t t-set="_order_id" t-value="doc.id"/>
<t t-set="_scan_id" t-value="doc.id"/> <t t-set="_scan_id" t-value="doc.id"/>
<t t-set="_mo" t-value="doc.production_id"/> <t t-set="_mo" t-value="doc.production_id"/>
@@ -328,6 +380,7 @@
<template id="report_fp_mo_sticker"> <template id="report_fp_mo_sticker">
<t t-call="web.html_container"> <t t-call="web.html_container">
<t t-foreach="docs" t-as="doc"> <t t-foreach="docs" t-as="doc">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<!-- Shop floor talks in "WO #" regardless of Odoo's MO/WO <!-- Shop floor talks in "WO #" regardless of Odoo's MO/WO
split. QR always encodes the numeric id so scans split. QR always encodes the numeric id so scans
resolve cleanly via /fp/wo/<id>. --> resolve cleanly via /fp/wo/<id>. -->
@@ -339,4 +392,39 @@
</t> </t>
</template> </template>
<!-- ========== Outer template — sale.order entry ==========
Prints one box sticker per order line that has a part. Lines
without x_fc_part_catalog_id (service lines, freight, etc.) are
skipped — they don't go through plating so they don't need a
box sticker.
The "WO #" header shows "<SO>/<line seq>" so the sticker
remains identifiable before the fp.job is generated. The QR
encodes /fp/so-line/<line.id> — the controller can decide
whether to land on the parent SO, the line, or (later) the
spawned job. -->
<template id="report_fp_so_sticker">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="so">
<t t-foreach="so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)"
t-as="line">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="so.name + ' / ' + str(line.sequence or line.id)"/>
<t t-set="_scan_id" t-value="line.id"/>
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
<t t-set="_mo" t-value="False"/>
<t t-set="_so" t-value="so"/>
<t t-set="_line" t-value="line"/>
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
<t t-set="_coating" t-value="line.x_fc_coating_config_id"/>
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
<t t-set="_qty" t-value="line.product_uom_qty"/>
<t t-set="_partner_name" t-value="so.partner_id.name"/>
<t t-set="_mo_ref" t-value="''"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
</t>
</t>
</template>
</odoo> </odoo>