diff --git a/fusion_plating/fusion_plating_jobs/__init__.py b/fusion_plating/fusion_plating_jobs/__init__.py
index a0fdc10f..7db66946 100644
--- a/fusion_plating/fusion_plating_jobs/__init__.py
+++ b/fusion_plating/fusion_plating_jobs/__init__.py
@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
+from . import report
diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py
index db9075d7..cc439664 100644
--- a/fusion_plating/fusion_plating_jobs/__manifest__.py
+++ b/fusion_plating/fusion_plating_jobs/__manifest__.py
@@ -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,
diff --git a/fusion_plating/fusion_plating_jobs/report/__init__.py b/fusion_plating/fusion_plating_jobs/report/__init__.py
new file mode 100644
index 00000000..24f7bb39
--- /dev/null
+++ b/fusion_plating/fusion_plating_jobs/report/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
diff --git a/fusion_plating/fusion_plating_jobs/report/report_fp_job_sticker.xml b/fusion_plating/fusion_plating_jobs/report/report_fp_job_sticker.xml
new file mode 100644
index 00000000..8e4906b4
--- /dev/null
+++ b/fusion_plating/fusion_plating_jobs/report/report_fp_job_sticker.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+ FP Job Sticker (6x4")
+ custom
+ 152
+ 102
+ Portrait
+ 0
+ 0
+ 0
+ 0
+
+ 0
+
+ 300
+
+
+
+ Job Sticker
+ fp.job
+ qweb-pdf
+ fusion_plating_jobs.report_fp_job_sticker_template
+ fusion_plating_jobs.report_fp_job_sticker_template
+ 'Job Sticker - %s' % (object.name or '').replace('/', '-')
+
+ report
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+ Customer:
+
+
+
+ SO:
+
+
+
+ Qty:
+
+
+
+ Due:
+
+
+
+ Recipe:
+
+
+
+ Steps:
+ /
+
+
+
+
+
+
+
diff --git a/fusion_plating/fusion_plating_jobs/report/report_fp_job_traveller.xml b/fusion_plating/fusion_plating_jobs/report/report_fp_job_traveller.xml
new file mode 100644
index 00000000..d83afb28
--- /dev/null
+++ b/fusion_plating/fusion_plating_jobs/report/report_fp_job_traveller.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+ Job Traveller
+ fp.job
+ qweb-pdf
+ fusion_plating_jobs.report_fp_job_traveller_template
+ fusion_plating_jobs.report_fp_job_traveller_template
+ 'Traveller - %s' % (object.name or '').replace('/', '-')
+
+ report
+
+
+
+
+
+
+
+
Job Traveller —
+
+ | Customer | |
+ | SO | |
+ | Qty | |
+ | Recipe | |
+ | Deadline | |
+ | Status | |
+
+
+
Steps
+
+
+
+ | # |
+ Operation |
+ Work Centre |
+ Kind |
+ Expected (min) |
+ Actual (min) |
+ State |
+ Operator Sign-off |
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py b/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py
index 1324a3c9..12ac2131 100644
--- a/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py
+++ b/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py
@@ -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)