diff --git a/fusion_plating/fusion_plating_jobs/README.md b/fusion_plating/fusion_plating_jobs/README.md new file mode 100644 index 00000000..c46dea1c --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/README.md @@ -0,0 +1,51 @@ +# fusion_plating_jobs + +Native plating job bridge — wires `fp.job` and `fp.job.step` (defined in +`fusion_plating` core, Phase 1 of the migration spec dated 2026-04-25) +into the rest of the Fusion Plating module family: configurator, portal, +logistics, quality, certificates, batches, KPI, notifications, reports. + +Coexists with `fusion_plating_bridge_mrp` during the migration period. +The `x_fc_use_native_jobs` settings flag (default: `False`) toggles the +behaviour. When `False`, SO confirm continues to create `mrp.production` +records through `bridge_mrp`. When `True`, SO confirm creates `fp.job` +records here. + +See `docs/superpowers/specs/2026-04-25-fp-native-job-model-design.md` +for full design rationale and §6 of the implementation plan for phase +breakdown. + +## Phase 6 — deferred items + +Phase 6 originally scoped the full operator UI rewrite. With Tailscale +SSH to entech currently unavailable we cannot live-test OWL/JS in the +browser, so Phase 6 ships a lean version: the data-layer endpoints land +now, the rendering UI lands later. + +Deferred to post-cutover hardening: + +- **Plant Overview kanban** over `fp.job.step` — replaces + `fusion_plating_shopfloor`'s `mrp.workorder` kanban. +- **Tablet Station UI** rewrite over `fp.job` / `fp.job.step`. +- **Manager Dashboard** rewrite. +- **Process Tree OWL component** — currently a stub: + `/fp/jobs/process_tree` returns the serialized recipe tree as JSON, + but the OWL component to render it is not built. + +Rationale: these are large OWL/JS components that need live in-browser +verification on entech. Under the migration's parallel-coexistence +strategy, operators continue using the existing shopfloor UI (bound to +`mrp.workorder`) until cutover. After cutover, the operator UI rewrite +becomes its own focused project — the data layer (`fp.job`, +`fp.job.step`, time logs, timestamps) is fully in place from +Phase 1–5. + +## Phase 6 — what shipped + +- `/fp/job/` — scan-redirect controller. The fp.job sticker QR + encodes this URL. Routes managers to the `fp.job` form; routes + operators to the same form for now (will swap to the process tree + client action once the OWL component lands). +- `/fp/jobs/process_tree` — JSON-RPC endpoint that returns the recipe + tree for a job, with each node tagged by its matching `fp.job.step` + state, ready for an OWL component to consume. diff --git a/fusion_plating/fusion_plating_jobs/__init__.py b/fusion_plating/fusion_plating_jobs/__init__.py index 7db66946..10666aa1 100644 --- a/fusion_plating/fusion_plating_jobs/__init__.py +++ b/fusion_plating/fusion_plating_jobs/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import models from . import report +from . import controllers diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index cc439664..b12aaec8 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.8.0', + 'version': '19.0.1.9.0', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'description': """ diff --git a/fusion_plating/fusion_plating_jobs/controllers/__init__.py b/fusion_plating/fusion_plating_jobs/controllers/__init__.py new file mode 100644 index 00000000..469ae9a4 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import job_scan +from . import process_tree diff --git a/fusion_plating/fusion_plating_jobs/controllers/job_scan.py b/fusion_plating/fusion_plating_jobs/controllers/job_scan.py new file mode 100644 index 00000000..69945db4 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/controllers/job_scan.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# +# /fp/job/ — scan-redirect endpoint for native fp.job stickers. +# +# The fp.job sticker (Phase 5) embeds a QR encoding this URL. When a +# warehouse user scans it, this controller redirects them to either +# the fp.job form (for managers) or the upcoming process-tree client +# action (for operators — Phase 6 expansion). + +from odoo import http +from odoo.http import request + + +class FpJobScanController(http.Controller): + + @http.route('/fp/job/', type='http', auth='user', website=False) + def fp_job_scan(self, job_id, **kwargs): + Job = request.env['fp.job'].sudo() + job = Job.browse(job_id).exists() + if not job: + return request.redirect('/odoo/plating-jobs') + + # If user is a plating manager → land on the form. + # Otherwise (operator) → land on process tree client action + # (will be wired once process tree is added). + user = request.env.user + is_manager = user.has_group('fusion_plating.group_fusion_plating_manager') + if is_manager: + return request.redirect( + '/odoo/action-fusion_plating.action_fp_job/%d' % job.id + ) + # Operator path: same form for now (process tree action will replace + # this once it's registered). + return request.redirect( + '/odoo/action-fusion_plating.action_fp_job/%d' % job.id + ) diff --git a/fusion_plating/fusion_plating_jobs/controllers/process_tree.py b/fusion_plating/fusion_plating_jobs/controllers/process_tree.py new file mode 100644 index 00000000..7653cb46 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/controllers/process_tree.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# +# /fp/jobs/process_tree — JSON endpoint that returns the recipe tree +# for a given fp.job, with each node tagged by the matching +# fp.job.step (if any) and its current state. + +from odoo import http +from odoo.http import request + + +class FpJobProcessTreeController(http.Controller): + + @http.route('/fp/jobs/process_tree', type='jsonrpc', auth='user', website=False) + def fp_jobs_process_tree(self, job_id, **kwargs): + Job = request.env['fp.job'] + job = Job.browse(int(job_id)).exists() + if not job: + return {'error': 'Job not found'} + + # Map recipe_node_id -> step + step_by_node = {s.recipe_node_id.id: s for s in job.step_ids if s.recipe_node_id} + + def serialize(node): + step = step_by_node.get(node.id) + return { + 'id': node.id, + 'name': node.name, + 'node_type': node.node_type, + 'sequence': node.sequence, + 'step_id': step.id if step else None, + 'step_state': step.state if step else None, + 'step_assigned_user': step.assigned_user_id.name if step and step.assigned_user_id else None, + 'duration_expected': step.duration_expected if step else node.estimated_duration, + 'duration_actual': step.duration_actual if step else 0.0, + 'children': [serialize(c) for c in node.child_ids.sorted('sequence')], + } + + return { + 'job_name': job.name, + 'partner': job.partner_id.name, + 'state': job.state, + 'qty': job.qty, + 'recipe_name': job.recipe_id.name if job.recipe_id else '', + 'progress_pct': job.step_progress_pct, + 'tree': serialize(job.recipe_id) if job.recipe_id else None, + } 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 12ac2131..61876c6f 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 @@ -532,3 +532,45 @@ class TestReports(TransactionCase): # 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) + + +class TestPhase6Controllers(TransactionCase): + def setUp(self): + super().setUp() + self.partner = self.env['res.partner'].create({'name': 'C'}) + self.product = self.env['product.product'].create({'name': 'P'}) + self.job = self.env['fp.job'].create({ + 'partner_id': self.partner.id, + 'product_id': self.product.id, + 'qty': 1.0, + }) + + def test_scan_controller_route_registered(self): + # Verify the route is registered in the controller registry. + # Odoo auto-registers @http.route decorated methods on module load. + # We don't HTTP-call from a TransactionCase; just confirm import works. + from odoo.addons.fusion_plating_jobs.controllers import job_scan, process_tree + self.assertTrue(hasattr(job_scan, 'FpJobScanController')) + self.assertTrue(hasattr(process_tree, 'FpJobProcessTreeController')) + + def test_process_tree_endpoint_logic(self): + # Direct method invocation (not HTTP) to verify serialization logic + # works for a job with steps + recipe. + recipe = self.env['fusion.plating.process.node'].create({ + 'name': 'R', 'node_type': 'recipe', + }) + op = self.env['fusion.plating.process.node'].create({ + 'name': 'Op1', 'node_type': 'operation', + 'parent_id': recipe.id, 'sequence': 10, + }) + self.job.recipe_id = recipe.id + self.env['fp.job.step'].create({ + 'job_id': self.job.id, 'name': 'Op1', 'sequence': 10, + 'recipe_node_id': op.id, + }) + # Direct call to the controller method body via a fake request + # context — in Odoo TransactionCase we can't easily simulate http.request, + # so this test just verifies the underlying step-serialization works. + step_by_node = {s.recipe_node_id.id: s for s in self.job.step_ids if s.recipe_node_id} + self.assertIn(op.id, step_by_node) + self.assertEqual(step_by_node[op.id].name, 'Op1')