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)
{
'name': 'Fusion Plating — Native Jobs',
'version': '19.0.5.0.0',
'version': '19.0.5.1.0',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.',

View File

@@ -3,12 +3,12 @@
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Native fp.job sticker — parallel to fusion_plating_reports' WO Box
Sticker which binds to mrp.production/mrp.workorder. Coexists during
the migration period.
QR encodes /fp/job/<id> (controller added in Task TBD; for now scan
will fall back to /web/login if controller absent).
Native fp.job sticker — reuses the canonical box-sticker design from
fusion_plating_reports.report_fp_wo_sticker_inner. The visual layout
(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
been printing since the mrp.production days; we just feed it from
fp.job fields here instead of mrp.production.
-->
<odoo>
@@ -43,73 +43,29 @@
<template id="report_fp_job_sticker_template">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="job">
<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/job/' + str(job.id)"/>
<t t-set="_qr_src" t-value="env['ir.actions.report'].barcode_data_uri('QR', _scan_url, width=300, height=300)"/>
<style>
@page { margin: 0; size: 152mm 102mm; }
html, body { margin: 0 !important; padding: 0 !important; width: 100% !important; height: 100% !important; }
.fp-job-sticker {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #000;
position: absolute;
top: 4px; left: 4px; right: 4px; bottom: 4px;
padding: 8px;
box-sizing: border-box;
border: 2px solid #000;
}
.fp-job-sticker-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1.5px solid #000;
padding-bottom: 6px;
margin-bottom: 8px;
}
.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>
<!-- Defaults block initialises every var the inner
reads (so `_so or ...` doesn't NameError). We
then override the ones we have data for. -->
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<!-- Pre-resolve the variables the shared inner template
expects, sourcing them from fp.job's native fields. -->
<t t-set="_order_id" t-value="job.name"/>
<t t-set="_scan_id" t-value="job.id"/>
<t t-set="_scan_path" t-value="'/fp/job/'"/>
<t t-set="_mo" t-value="False"/>
<t t-set="_so" t-value="job.sale_order_id"/>
<t t-set="_line" t-value="job.sale_order_line_ids[:1]"/>
<t t-set="_part" t-value="('part_catalog_id' in job._fields and job.part_catalog_id) or False"/>
<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"/>
<t t-set="_due" t-value="job.date_deadline or False"/>
<t t-set="_qty" t-value="job.qty"/>
<t t-set="_partner_name" t-value="job.partner_id.name"/>
<!-- The fp.job's own name (WH/JOB/00033) is already
printed in the header as "WO #...", so suppress
the muted "(WH/MO/...)" suffix on the PO row. -->
<t t-set="_mo_ref" t-value="''"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
</t>
</template>

View File

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

View File

@@ -369,6 +369,24 @@
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
</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) -->
<!-- ============================================================= -->

View File

@@ -5,38 +5,70 @@
Parts-box identification sticker — printed on a 4x3" label.
Bound to BOTH mrp.production (MO) and mrp.workorder (WO) because
the shop talks in "WO #" terms (Steelhead legacy) but the data
hangs off the MO record. The inner template normalises either
input to the same set of resolved variables:
Bound to mrp.production (MO), mrp.workorder (WO), fp.job, and
sale.order. The shop talks in "WO #" terms (Steelhead legacy) but
the data may hang off any of those records. The inner template
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 #"
* _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
* _part — fp.part.catalog
* _coating — fp.coating.config
* _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>
<!-- ========== Shared inner template ========== -->
<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="_scan_url" t-value="_base_url + '/fp/wo/' + str(_scan_id)"/>
<t t-set="_so" t-value="_mo and env['sale.order'].sudo().search(
[('name', '=', _mo.origin)], limit=1) or False"/>
<t t-set="_line" t-value="(_mo and 'x_fc_sale_order_line_ids' in _mo._fields
and _mo.x_fc_sale_order_line_ids[:1])
<t t-set="_scan_path" t-value="_scan_path or '/fp/wo/'"/>
<t t-set="_scan_url" t-value="_base_url + _scan_path + str(_scan_id)"/>
<!-- Each variable: prefer the outer-supplied value, otherwise
resolve from _mo. This lets fp.job / sale.order outers feed
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 False"/>
<t t-set="_part" t-value="_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="_process" t-value="(_part and _part.default_process_id)
<t t-set="_part" t-value="_part or (_line and _line.x_fc_part_catalog_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="_process
or (_part and _part.default_process_id)
or (_coating and _coating.recipe_id)
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 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
to fetch /report/barcode/ over the network during rendering. -->
<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-value">
<span class="fp-sticker-strong"
t-esc="(_so and _so.x_fc_po_number) or '-'"/>
<t t-if="_mo">
t-esc="_po_number"/>
<t t-if="_mo_ref">
<span class="fp-sticker-muted">
(<span t-esc="_mo.name"/>)
(<span t-esc="_mo_ref"/>)
</span>
</t>
</td>
@@ -252,7 +284,7 @@
<tr>
<td class="fp-sticker-label">Customer:</td>
<td class="fp-sticker-value">
<span t-esc="(_so and _so.partner_id.name) or '-'"/>
<span t-esc="_partner_name"/>
</td>
</tr>
<tr>
@@ -295,7 +327,6 @@
<td class="fp-sticker-label">Qty:</td>
<td class="fp-sticker-value">
<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>
</td>
@@ -303,8 +334,7 @@
<tr>
<td class="fp-sticker-label">Notes:</td>
<td class="fp-sticker-value">
<t t-esc="(_so and _so.x_fc_internal_note
and _so.x_fc_internal_note.striptags()[:100]) or '-'"/>
<t t-esc="_internal_note"/>
</td>
</tr>
</table>
@@ -312,10 +342,32 @@
</div>
</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 ========== -->
<template id="report_fp_wo_sticker">
<t t-call="web.html_container">
<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="_scan_id" t-value="doc.id"/>
<t t-set="_mo" t-value="doc.production_id"/>
@@ -328,6 +380,7 @@
<template id="report_fp_mo_sticker">
<t t-call="web.html_container">
<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
split. QR always encodes the numeric id so scans
resolve cleanly via /fp/wo/<id>. -->
@@ -339,4 +392,39 @@
</t>
</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>