feat(reports): MO-bound WO sticker + polished professional layout
User reported two issues with the sticker:
1. "Print → WO Box Sticker" didn't appear on the MO form
(WH/MO/00067). The operator workflow lives on the MO form, not
the WO — binding only to mrp.workorder meant they couldn't see
the option. Now bound to BOTH:
* mrp.workorder (per-WO sticker)
* mrp.production (per-MO sticker — prints the MO friendly
name after "WO #" so it reads naturally in shop-floor
vocabulary)
Internal refactor: factored the layout into a shared inner
template report_fp_wo_sticker_inner; the two outer templates
normalise their input to the same _order_id / _scan_id / _mo
variables and t-call the inner.
2. Design polish. The previous layout was a plain label/value
table that looked rough. Redesigned with:
* Proper sticker chrome: 0.5mm black border, 1.5mm rounded
corners, edge padding.
* Header row with bottom border rule separating logo+WO-# on
the left from QR+caption on the right.
* Grid rows now alternate white / #f4f5f7 zebra-striping with
a right-aligned vertical rule between label and value.
* ALL-CAPS, letter-spaced, gray-333 labels at 7.5pt; values
at 8.5pt with strong (9.5pt, 700) emphasis on the key data
(PO, Part Number, Qty) so it reads at a glance from across
the warehouse.
* Helvetica Neue font stack.
* "SCAN TO OPEN" caption under the QR.
Scan endpoint updated: /fp/wo/<id> now tries mrp.production first
(operator home form) then falls back to mrp.workorder. Numeric
collisions between the two id spaces are possible; MO wins because
the MO view carries the full context.
fusion_plating_reports → 19.0.7.1.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Reports',
|
||||
'version': '19.0.7.0.0',
|
||||
'version': '19.0.7.1.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
|
||||
'depends': [
|
||||
|
||||
@@ -20,19 +20,27 @@ class FpWoScanController(http.Controller):
|
||||
|
||||
@http.route('/fp/wo/<int:wo_id>', type='http', auth='user', website=False)
|
||||
def wo_scan_redirect(self, wo_id, **kwargs):
|
||||
"""Redirect a scanned WO sticker to the work-order form.
|
||||
"""Redirect a scanned sticker to the right backend form.
|
||||
|
||||
Uses Odoo 17+/19's action-URL format so the backend opens
|
||||
directly on the WO's form view. Falls back to a generic
|
||||
not-found URL if the id doesn't resolve.
|
||||
Stickers are printed from two sources — mrp.workorder (WO) and
|
||||
mrp.production (MO) — and both embed their own numeric id in
|
||||
the QR. Try the MO table first (operators live on the MO
|
||||
form — customer, SO, all WOs visible) and fall back to WO.
|
||||
"""
|
||||
wo = request.env['mrp.workorder'].sudo().browse(wo_id).exists()
|
||||
if not wo:
|
||||
# Land on the list of all WOs so staff can search manually.
|
||||
return request.redirect('/odoo/manufacturing/work-orders')
|
||||
# /odoo/action-<xmlid>/<id> opens the record's form view.
|
||||
# Using the vanilla MRP action here so it works regardless of
|
||||
# whether the user has Plating-specific menus.
|
||||
MO = request.env['mrp.production'].sudo()
|
||||
WO = request.env['mrp.workorder'].sudo()
|
||||
|
||||
mo = MO.browse(wo_id).exists()
|
||||
if mo:
|
||||
return request.redirect(
|
||||
'/odoo/action-mrp.mrp_production_action/%d' % mo.id
|
||||
)
|
||||
|
||||
wo = WO.browse(wo_id).exists()
|
||||
if wo:
|
||||
return request.redirect(
|
||||
'/odoo/action-mrp.action_mrp_workorder/%d' % wo.id
|
||||
)
|
||||
|
||||
# Neither resolved — land on the WO list so staff can search manually.
|
||||
return request.redirect('/odoo/manufacturing/work-orders')
|
||||
|
||||
@@ -348,6 +348,22 @@
|
||||
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
|
||||
</record>
|
||||
|
||||
<!-- Same sticker bound to mrp.production so "Print → WO Box
|
||||
Sticker" appears on the MO form too. Operators live on the
|
||||
MO form (WH/MO/00067 etc.); binding to both surfaces keeps
|
||||
the workflow frictionless. -->
|
||||
<record id="action_report_fp_mo_sticker" model="ir.actions.report">
|
||||
<field name="name">WO Box Sticker</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_plating_reports.report_fp_mo_sticker</field>
|
||||
<field name="report_file">fusion_plating_reports.report_fp_mo_sticker</field>
|
||||
<field name="print_report_name">'WO Sticker - %s' % (object.name or '').replace('/', '-')</field>
|
||||
<field name="binding_model_id" ref="mrp.model_mrp_production"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- 15. Packing Slip (Portrait + Landscape) -->
|
||||
<!-- ============================================================= -->
|
||||
|
||||
@@ -3,102 +3,178 @@
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
WO Box Sticker — 4x3" parts-box identification label.
|
||||
Parts-box identification sticker — printed on a 4x3" label.
|
||||
|
||||
Layout mirrors the Steelhead sticker format the client is
|
||||
migrating from:
|
||||
* ENTECH logo top-left.
|
||||
* QR code top-right — encodes the WO's scan URL
|
||||
(/fp/wo/<id>) so warehouse staff can scan with any
|
||||
phone/tablet and land on the WO form instantly.
|
||||
* Grid of PO / Customer / Process / Part Number / Due Date
|
||||
/ Qty / Notes rows.
|
||||
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:
|
||||
* _order_id — number to print as "WO #"
|
||||
* _mo — the mrp.production record
|
||||
* _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)
|
||||
|
||||
Printed on a dedicated 4x3" paperformat; no header / footer.
|
||||
The sticker works identically whether triggered from the MO form
|
||||
or from any of its child WOs.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<template id="report_fp_wo_sticker">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-set="_mo" t-value="doc.production_id"/>
|
||||
<!-- ========== 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', '')"/>
|
||||
<!-- _scan_id is always a numeric database id (mo.id or wo.id)
|
||||
so the scan endpoint can resolve it cleanly. _order_id is
|
||||
what gets printed next to "WO #" and can be a friendly
|
||||
Odoo name like "WH/MO/00067". -->
|
||||
<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="_so and _so.order_line[:1] or False"/>
|
||||
<t t-set="_part" t-value="
|
||||
(_line and _line.x_fc_part_catalog_id)
|
||||
or (doc.production_id and 'x_fc_part_catalog_ids' in _mo._fields
|
||||
and _mo.x_fc_sale_order_line_ids[:1].x_fc_part_catalog_id)
|
||||
<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])
|
||||
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)
|
||||
or (_coating and _coating.recipe_id)
|
||||
or False"/>
|
||||
<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(doc.id)"/>
|
||||
|
||||
<div class="page" style="font-family: Arial, sans-serif;
|
||||
color: #000; width: 100%; height: 100%; padding: 4mm;
|
||||
box-sizing: border-box;">
|
||||
<t t-set="_due" t-value="(_mo and (_mo.date_deadline or _mo.date_finished))
|
||||
or (_line and _line.x_fc_part_deadline)
|
||||
or False"/>
|
||||
|
||||
<style>
|
||||
.fp-stk-row { display: table-row; }
|
||||
.fp-stk-lbl {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
padding: 1mm 2mm 1mm 0;
|
||||
white-space: nowrap;
|
||||
.fp-sticker {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
color: #111;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 3mm 4mm;
|
||||
box-sizing: border-box;
|
||||
border: 0.5mm solid #000;
|
||||
border-radius: 1.5mm;
|
||||
}
|
||||
.fp-stk-val {
|
||||
.fp-sticker-header {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-bottom: 0.3mm solid #000;
|
||||
padding-bottom: 2mm;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
.fp-sticker-header-left {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
width: 66%;
|
||||
padding-right: 2mm;
|
||||
}
|
||||
.fp-sticker-header-right {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
width: 34%;
|
||||
text-align: right;
|
||||
}
|
||||
.fp-sticker-logo {
|
||||
max-height: 11mm;
|
||||
max-width: 100%;
|
||||
}
|
||||
.fp-sticker-wo {
|
||||
font-size: 15pt;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3mm;
|
||||
margin-top: 1.5mm;
|
||||
}
|
||||
.fp-sticker-qr {
|
||||
width: 22mm;
|
||||
height: 22mm;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
.fp-sticker-qr-caption {
|
||||
font-size: 6pt;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
margin-top: 0.3mm;
|
||||
letter-spacing: 0.1mm;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.fp-sticker-grid {
|
||||
display: table;
|
||||
width: 100%;
|
||||
font-size: 8.5pt;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.fp-sticker-row { display: table-row; }
|
||||
.fp-sticker-row-alt .fp-sticker-label,
|
||||
.fp-sticker-row-alt .fp-sticker-value {
|
||||
background-color: #f4f5f7;
|
||||
}
|
||||
.fp-sticker-label {
|
||||
display: table-cell;
|
||||
width: 28%;
|
||||
vertical-align: top;
|
||||
padding: 1mm 2mm 1mm 2mm;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15mm;
|
||||
text-transform: uppercase;
|
||||
font-size: 7.5pt;
|
||||
color: #333;
|
||||
border-right: 0.3mm solid #000;
|
||||
}
|
||||
.fp-sticker-value {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
padding: 1mm 0 1mm 2mm;
|
||||
border-left: 0.4mm solid #000;
|
||||
padding: 1mm 2mm 1mm 2.5mm;
|
||||
}
|
||||
.fp-sticker-strong {
|
||||
font-weight: 700;
|
||||
font-size: 9.5pt;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Header: logo + WO # + QR -->
|
||||
<div style="display: table; width: 100%;">
|
||||
<!-- Logo + WO number -->
|
||||
<div style="display: table-cell; vertical-align: top;
|
||||
width: 65%; padding-right: 2mm;">
|
||||
<div class="fp-sticker">
|
||||
<!-- Header — logo + WO number + QR -->
|
||||
<div class="fp-sticker-header">
|
||||
<div class="fp-sticker-header-left">
|
||||
<img t-if="env.company.logo"
|
||||
t-att-src="image_data_uri(env.company.logo)"
|
||||
style="max-height: 14mm; max-width: 100%;"/>
|
||||
<div style="font-size: 14pt; font-weight: bold;
|
||||
margin-top: 2mm;">
|
||||
WO #<span t-esc="doc.id"/>
|
||||
class="fp-sticker-logo"
|
||||
t-att-src="image_data_uri(env.company.logo)"/>
|
||||
<div class="fp-sticker-wo">
|
||||
WO #<span t-esc="_order_id"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- QR code — linked to the scan redirect endpoint -->
|
||||
<div style="display: table-cell; vertical-align: top;
|
||||
width: 35%; text-align: right;">
|
||||
<img t-att-src="'/report/barcode/?barcode_type=QR&value=%s&width=200&height=200' % quote(_scan_url)"
|
||||
style="width: 26mm; height: 26mm;"/>
|
||||
<div class="fp-sticker-header-right">
|
||||
<img class="fp-sticker-qr"
|
||||
t-att-src="'/report/barcode/?barcode_type=QR&value=%s&width=300&height=300' % quote(_scan_url)"/>
|
||||
<div class="fp-sticker-qr-caption">scan to open</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data grid -->
|
||||
<div style="display: table; width: 100%;
|
||||
margin-top: 3mm; font-size: 9pt;">
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">PO (RO):</div>
|
||||
<div class="fp-stk-val">
|
||||
<t t-esc="(_so and _so.x_fc_po_number) or '—'"/>
|
||||
<t t-if="_mo"> (<span t-esc="_mo.id"/>)</t>
|
||||
<div class="fp-sticker-grid">
|
||||
<div class="fp-sticker-row">
|
||||
<div class="fp-sticker-label">PO (RO)</div>
|
||||
<div class="fp-sticker-value">
|
||||
<span class="fp-sticker-strong"
|
||||
t-esc="(_so and _so.x_fc_po_number) or '—'"/>
|
||||
<t t-if="_mo">
|
||||
<span style="color:#666;">
|
||||
(<span t-esc="_mo.name"/>)
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Customer:</div>
|
||||
<div class="fp-stk-val">
|
||||
<t t-esc="(_so and _so.partner_id.name) or '—'"/>
|
||||
<div class="fp-sticker-row fp-sticker-row-alt">
|
||||
<div class="fp-sticker-label">Customer</div>
|
||||
<div class="fp-sticker-value">
|
||||
<span t-esc="(_so and _so.partner_id.name) or '—'"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Process:</div>
|
||||
<div class="fp-stk-val">
|
||||
<div class="fp-sticker-row">
|
||||
<div class="fp-sticker-label">Process</div>
|
||||
<div class="fp-sticker-value">
|
||||
<t t-if="_process">
|
||||
<span t-esc="_process.name"/>
|
||||
</t>
|
||||
@@ -108,41 +184,82 @@
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Part Number:</div>
|
||||
<div class="fp-stk-val">
|
||||
<div class="fp-sticker-row fp-sticker-row-alt">
|
||||
<div class="fp-sticker-label">Part Number</div>
|
||||
<div class="fp-sticker-value">
|
||||
<t t-if="_part">
|
||||
<span t-esc="_part.part_number"/>
|
||||
<t t-if="_part.revision"> Rev <span t-esc="_part.revision"/></t>
|
||||
<span class="fp-sticker-strong"
|
||||
t-esc="_part.part_number"/>
|
||||
<t t-if="_part.revision">
|
||||
<span style="color:#666;">
|
||||
Rev <span t-esc="_part.revision"/>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Due Date:</div>
|
||||
<div class="fp-stk-val">
|
||||
<t t-set="_due" t-value="_mo and (_mo.date_deadline or _mo.date_finished) or False"/>
|
||||
<div class="fp-sticker-row">
|
||||
<div class="fp-sticker-label">Due Date</div>
|
||||
<div class="fp-sticker-value">
|
||||
<t t-if="_due">
|
||||
<span t-esc="_due.strftime('%m/%d/%Y')"/>
|
||||
<span t-esc="_due.strftime('%b %d, %Y')"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Qty:</div>
|
||||
<div class="fp-stk-val">
|
||||
<span t-esc="int(doc.qty_production) if doc.qty_production == int(doc.qty_production) else doc.qty_production"/>
|
||||
<div class="fp-sticker-row fp-sticker-row-alt">
|
||||
<div class="fp-sticker-label">Qty</div>
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Notes:</div>
|
||||
<div class="fp-stk-val">
|
||||
<div class="fp-sticker-row">
|
||||
<div class="fp-sticker-label">Notes</div>
|
||||
<div class="fp-sticker-value"
|
||||
style="font-size: 7.5pt; color: #333;">
|
||||
<t t-esc="(_so and _so.x_fc_internal_note
|
||||
and _so.x_fc_internal_note.striptags()[:120]) or ''"/>
|
||||
and _so.x_fc_internal_note.striptags()[:140]) or '—'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<div class="page"
|
||||
style="padding:0; margin:0; width:100%; height:100%;">
|
||||
<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"/>
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- ========== Outer template — mrp.production entry ========== -->
|
||||
<template id="report_fp_mo_sticker">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<div class="page"
|
||||
style="padding:0; margin:0; width:100%; height:100%;">
|
||||
<!-- Print the MO's friendly name after "WO #" because
|
||||
the shop floor terminology is "WO" for the top-
|
||||
level order, regardless of Odoo's MO/WO split.
|
||||
The QR still encodes the numeric id so scanning
|
||||
resolves cleanly. -->
|
||||
<t t-set="_order_id" t-value="doc.name or doc.id"/>
|
||||
<t t-set="_scan_id" t-value="doc.id"/>
|
||||
<t t-set="_mo" t-value="doc"/>
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user