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:
gsinghpal
2026-04-23 10:39:35 -04:00
parent eddf803d4c
commit be33a76ad2
6 changed files with 229 additions and 1 deletions

View File

@@ -3,3 +3,4 @@
# License OPL-1 (Odoo Proprietary License v1.0)
from . import models
from . import controllers

View File

@@ -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',

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import wo_scan

View 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
)

View File

@@ -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) -->
<!-- ============================================================= -->

View File

@@ -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&amp;value=%s&amp;width=200&amp;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>