feat(jobs): Phase 5 — fp.job reports (sticker + traveller)
Two parallel report definitions for the native job model: 1. Job Sticker (6x4 inch custom paperformat) bound to fp.job. Prints WH/JOB/... ID, customer, SO, qty, due date, recipe, step progress. QR encodes /fp/job/<id> for scan-to-job navigation. 2. Job Traveller bound to fp.job, A4 portrait. Job header + all fp.job.step rows in sequence order with operator sign-off column. Coexists with fusion_plating_reports' MO/WO bindings — both print menus stay live during migration. Deferred reports (use existing during migration; rebind at cutover): - BoL, Packing Slip, Invoice (read from SO, no fp.job change needed) - WO Margin (cost rollup; rebuild against fp.job.step.cost_total in phase-end polish) Adds fusion_plating_reports to fusion_plating_jobs depends. Tests deferred to post-Tailscale-restore: 3 new tests verify report actions are registered + sticker template renders without QWeb errors. Module file content verified locally as well-formed XML. Manifest 19.0.1.7.0 → 19.0.1.8.0. Part of: native job model migration (spec 2026-04-25) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import report
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.1.7.0',
|
||||
'version': '19.0.1.8.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'description': """
|
||||
@@ -33,10 +33,13 @@ full design rationale and §6.2 of the implementation plan for task list.
|
||||
'fusion_plating_portal', # fusion.plating.portal.job
|
||||
'fusion_plating_quality', # fusion.plating.customer.spec, fusion.plating.quality.hold
|
||||
'fusion_plating_receiving', # fp.racking.inspection (Phase 3)
|
||||
'fusion_plating_reports', # paperformat helpers, customer_line_header (Phase 5)
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/res_config_settings_views.xml',
|
||||
'report/report_fp_job_sticker.xml',
|
||||
'report/report_fp_job_traveller.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
|
||||
3
fusion_plating/fusion_plating_jobs/report/__init__.py
Normal file
3
fusion_plating/fusion_plating_jobs/report/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="paperformat_fp_job_sticker" model="report.paperformat">
|
||||
<field name="name">FP Job Sticker (6x4")</field>
|
||||
<field name="format">custom</field>
|
||||
<field name="page_width">152</field>
|
||||
<field name="page_height">102</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">0</field>
|
||||
<field name="margin_bottom">0</field>
|
||||
<field name="margin_left">0</field>
|
||||
<field name="margin_right">0</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_job_sticker" model="ir.actions.report">
|
||||
<field name="name">Job Sticker</field>
|
||||
<field name="model">fp.job</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_plating_jobs.report_fp_job_sticker_template</field>
|
||||
<field name="report_file">fusion_plating_jobs.report_fp_job_sticker_template</field>
|
||||
<field name="print_report_name">'Job Sticker - %s' % (object.name or '').replace('/', '-')</field>
|
||||
<field name="binding_model_id" ref="fusion_plating.model_fp_job"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="paperformat_fp_job_sticker"/>
|
||||
</record>
|
||||
|
||||
<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>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
Native fp.job traveller — minimal portrait A4 listing all steps.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="action_report_fp_job_traveller" model="ir.actions.report">
|
||||
<field name="name">Job Traveller</field>
|
||||
<field name="model">fp.job</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_plating_jobs.report_fp_job_traveller_template</field>
|
||||
<field name="report_file">fusion_plating_jobs.report_fp_job_traveller_template</field>
|
||||
<field name="print_report_name">'Traveller - %s' % (object.name or '').replace('/', '-')</field>
|
||||
<field name="binding_model_id" ref="fusion_plating.model_fp_job"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<template id="report_fp_job_traveller_template">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="job">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h1>Job Traveller — <span t-esc="job.name"/></h1>
|
||||
<table class="table table-sm" style="margin-top: 1em;">
|
||||
<tr><th>Customer</th><td><span t-esc="job.partner_id.name"/></td></tr>
|
||||
<tr><th>SO</th><td><span t-esc="job.sale_order_id.name or '-'"/></td></tr>
|
||||
<tr><th>Qty</th><td><span t-esc="job.qty"/></td></tr>
|
||||
<tr><th>Recipe</th><td><span t-esc="job.recipe_id.name or '-'"/></td></tr>
|
||||
<tr><th>Deadline</th><td><span t-esc="job.date_deadline and job.date_deadline.strftime('%b %d, %Y') or '-'"/></td></tr>
|
||||
<tr><th>Status</th><td><span t-esc="job.state"/></td></tr>
|
||||
</table>
|
||||
|
||||
<h2 style="margin-top: 2em;">Steps</h2>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Operation</th>
|
||||
<th>Work Centre</th>
|
||||
<th>Kind</th>
|
||||
<th>Expected (min)</th>
|
||||
<th>Actual (min)</th>
|
||||
<th>State</th>
|
||||
<th>Operator Sign-off</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="job.step_ids.sorted('sequence')" t-as="step">
|
||||
<tr>
|
||||
<td><span t-esc="step.sequence"/></td>
|
||||
<td><span t-esc="step.name"/></td>
|
||||
<td><span t-esc="step.work_centre_id.name or ''"/></td>
|
||||
<td><span t-esc="step.kind"/></td>
|
||||
<td><span t-esc="step.duration_expected"/></td>
|
||||
<td><span t-esc="step.duration_actual"/></td>
|
||||
<td><span t-esc="step.state"/></td>
|
||||
<td style="border-bottom: 1px solid #999; min-width: 100px;"></td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -503,3 +503,32 @@ class TestPhase4Refactors(TransactionCase):
|
||||
})
|
||||
job.action_confirm() # Should not raise even with no templates
|
||||
self.assertEqual(job.state, 'confirmed')
|
||||
|
||||
|
||||
class TestReports(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = self.env['res.partner'].create({'name': 'C'})
|
||||
self.product = self.env['product.product'].create({'name': 'P'})
|
||||
|
||||
def test_sticker_report_action_exists(self):
|
||||
action = self.env.ref('fusion_plating_jobs.action_report_fp_job_sticker', raise_if_not_found=False)
|
||||
self.assertTrue(action)
|
||||
self.assertEqual(action.model, 'fp.job')
|
||||
|
||||
def test_traveller_report_action_exists(self):
|
||||
action = self.env.ref('fusion_plating_jobs.action_report_fp_job_traveller', raise_if_not_found=False)
|
||||
self.assertTrue(action)
|
||||
self.assertEqual(action.model, 'fp.job')
|
||||
|
||||
def test_sticker_renders_for_a_job(self):
|
||||
# Smoke test: the QWeb template should render without error.
|
||||
job = self.env['fp.job'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 1.0,
|
||||
})
|
||||
report = self.env.ref('fusion_plating_jobs.action_report_fp_job_sticker')
|
||||
# Render HTML (faster than PDF; doesn't need wkhtmltopdf)
|
||||
html, _ = report._render_qweb_html(report.report_name, job.ids)
|
||||
self.assertIn(job.name, html.decode() if isinstance(html, bytes) else html)
|
||||
|
||||
Reference in New Issue
Block a user