feat(reports): WO box sticker + QR-scan-to-WO endpoint
Client is migrating from Steelhead and needs to keep the small
parts-box sticker format the warehouse crew already knows. Two
pieces shipped together so scanning is seamless from day one:
1. report_fp_wo_sticker — 4x3" QWeb label bound to mrp.workorder.
Layout mirrors the Steelhead sticker:
* ENTECH logo top-left (via env.company.logo)
* QR code top-right encoding /fp/wo/<id>
* Grid: PO (RO) / Customer / Process / Part Number / Due
Date / Qty / Notes
Dedicated paperformat_fp_wo_sticker at 102x76mm, 300 DPI,
landscape, 3mm margins — sized for thermal / inkjet label
printers without shrink-to-fit.
Binding added so "Print → WO Box Sticker" appears on every
mrp.workorder record.
2. FpWoScanController — GET /fp/wo/<int:wo_id> redirects the
scanner straight to the work-order form
(/odoo/action-mrp.action_mrp_workorder/<id>). auth='user' so
logged-in scanners land on the WO immediately; others bounce
through Odoo's login and return to the same URL. No custom
client work needed — any phone camera, handheld barcode
scanner, or tablet browser opens the URL on scan.
Process row resolution chain: part.default_process_id →
coating.recipe_id → fallback. So the sticker prints whichever
process is actually going to drive WO generation for this line,
matching the direct-order wizard's Effective Process column.
fusion_plating_reports → 19.0.7.0.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,3 +3,4 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Reports',
|
||||
'version': '19.0.6.0.0',
|
||||
'version': '19.0.7.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
|
||||
'depends': [
|
||||
@@ -42,6 +42,7 @@
|
||||
'report/customer_line_header.xml',
|
||||
'report/report_fp_sale.xml',
|
||||
'report/report_fp_work_order.xml',
|
||||
'report/report_fp_wo_sticker.xml',
|
||||
'report/report_fp_job_traveller.xml',
|
||||
'report/report_fp_packing_slip.xml',
|
||||
'report/report_fp_bol.xml',
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import wo_scan
|
||||
38
fusion_plating/fusion_plating_reports/controllers/wo_scan.py
Normal file
38
fusion_plating/fusion_plating_reports/controllers/wo_scan.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
#
|
||||
# /fp/wo/<id> — scan-redirect endpoint.
|
||||
#
|
||||
# The WO box sticker embeds a QR code that encodes this URL. When
|
||||
# warehouse staff scan the sticker with their phone / tablet /
|
||||
# handheld scanner, the device opens the URL; this controller then
|
||||
# redirects them to the work-order form inside Odoo's backend.
|
||||
# Logged-out users land on the standard Odoo login page and bounce
|
||||
# back after authenticating (Odoo's redirect handles the round-trip).
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
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.
|
||||
return request.redirect(
|
||||
'/odoo/action-mrp.action_mrp_workorder/%d' % wo.id
|
||||
)
|
||||
@@ -315,6 +315,39 @@
|
||||
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- 14b. Box Sticker — 4x3" label for parts-box identification -->
|
||||
<!-- Prints an ENTECH-style sticker with a QR code that -->
|
||||
<!-- warehouse staff scan to jump straight to the WO form. -->
|
||||
<!-- ============================================================= -->
|
||||
<record id="paperformat_fp_wo_sticker" model="report.paperformat">
|
||||
<field name="name">FP WO Sticker (4x3")</field>
|
||||
<field name="format">custom</field>
|
||||
<field name="page_width">102</field>
|
||||
<field name="page_height">76</field>
|
||||
<field name="orientation">Landscape</field>
|
||||
<field name="margin_top">3</field>
|
||||
<field name="margin_bottom">3</field>
|
||||
<field name="margin_left">3</field>
|
||||
<field name="margin_right">3</field>
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">0</field>
|
||||
<field name="disable_shrinking" eval="True"/>
|
||||
<field name="dpi">300</field>
|
||||
</record>
|
||||
|
||||
<record id="action_report_fp_wo_sticker" model="ir.actions.report">
|
||||
<field name="name">WO Box Sticker</field>
|
||||
<field name="model">mrp.workorder</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_plating_reports.report_fp_wo_sticker</field>
|
||||
<field name="report_file">fusion_plating_reports.report_fp_wo_sticker</field>
|
||||
<field name="print_report_name">'WO Sticker - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="mrp.model_mrp_workorder"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- 15. Packing Slip (Portrait + Landscape) -->
|
||||
<!-- ============================================================= -->
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
WO Box Sticker — 4x3" parts-box identification 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.
|
||||
|
||||
Printed on a dedicated 4x3" paperformat; no header / footer.
|
||||
-->
|
||||
<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"/>
|
||||
<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)
|
||||
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;">
|
||||
|
||||
<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-stk-val {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
padding: 1mm 0 1mm 2mm;
|
||||
border-left: 0.4mm solid #000;
|
||||
}
|
||||
</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;">
|
||||
<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"/>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Process:</div>
|
||||
<div class="fp-stk-val">
|
||||
<t t-if="_process">
|
||||
<span t-esc="_process.name"/>
|
||||
</t>
|
||||
<t t-elif="_coating">
|
||||
<span t-esc="_coating.name"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Part Number:</div>
|
||||
<div class="fp-stk-val">
|
||||
<t t-if="_part">
|
||||
<span t-esc="_part.part_number"/>
|
||||
<t t-if="_part.revision"> Rev <span t-esc="_part.revision"/></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"/>
|
||||
<t t-if="_due">
|
||||
<span t-esc="_due.strftime('%m/%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>
|
||||
</div>
|
||||
<div class="fp-stk-row">
|
||||
<div class="fp-stk-lbl">Notes:</div>
|
||||
<div class="fp-stk-val">
|
||||
<t t-esc="(_so and _so.x_fc_internal_note
|
||||
and _so.x_fc_internal_note.striptags()[:120]) or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user