diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index 864bdc41..db9075d7 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.6.0', + 'version': '19.0.1.7.0', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'description': """ @@ -27,7 +27,9 @@ full design rationale and §6.2 of the implementation plan for task list. 'fusion_plating_batch', # fusion.plating.batch (Phase 3) 'fusion_plating_certificates', # fp.certificate, fp.thickness.reading 'fusion_plating_configurator', # fp.part.catalog, fp.coating.config + 'fusion_plating_kpi', # fusion.plating.kpi.value (Phase 4) 'fusion_plating_logistics', # fusion.plating.delivery + 'fusion_plating_notifications', # fp.notification.template (Phase 4) '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) diff --git a/fusion_plating/fusion_plating_jobs/models/__init__.py b/fusion_plating/fusion_plating_jobs/models/__init__.py index 5d87aad9..7cc8f7fc 100644 --- a/fusion_plating/fusion_plating_jobs/models/__init__.py +++ b/fusion_plating/fusion_plating_jobs/models/__init__.py @@ -19,3 +19,7 @@ from . import fp_certificate from . import fp_thickness_reading from . import fp_delivery from . import fp_racking_inspection + +# Phase 4 — light refactors batch B (notifications, KPI source tag). +from . import fp_notification_trigger +from . import fusion_plating_kpi_value diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py index 457001f0..89033fdd 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py @@ -260,6 +260,7 @@ class FpJob(models.Model): job._fp_create_portal_job() job._fp_create_qc_check_if_needed() job._fp_create_racking_inspection() + job._fp_fire_notification('job_confirmed') return result def _fp_create_racking_inspection(self): @@ -382,8 +383,43 @@ class FpJob(models.Model): job.date_finished = fields.Datetime.now() job._fp_create_delivery() job._fp_create_certificates() + job._fp_fire_notification('job_complete') return True + # ------------------------------------------------------------------ + # Notifications dispatch (Phase 4) + # + # Fires fp.notification.template records whose trigger_event matches + # the given event name. Best-effort: silently skips if the + # fusion_plating_notifications module is not installed (model not + # registered) and logs (without raising) on any send failure so the + # job lifecycle is never blocked by an email problem. + # ------------------------------------------------------------------ + def _fp_fire_notification(self, event): + """Best-effort notification dispatch for fp.job lifecycle events. + + Looks up fp.notification.template records with the matching + trigger_event and dispatches via the central _dispatch helper + provided by fusion_plating_notifications. Silently no-ops when + that module isn't installed. + """ + self.ensure_one() + if 'fp.notification.template' not in self.env: + return + Template = self.env['fp.notification.template'].sudo() + try: + # The notifications module exposes a model-level _dispatch + # helper that handles template lookup, recipient resolution + # (Sub 6 contact routing), attachment rendering, and audit + # logging in one go. Pass partner explicitly since fp.job's + # partner_id is the customer. + Template._dispatch(event, self, partner=self.partner_id) + except Exception as e: + _logger.warning( + "Job %s: notification %s dispatch failed: %s", + self.name, event, e, + ) + def _fp_create_delivery(self): """Create a draft fusion.plating.delivery linked to this job.""" self.ensure_one() diff --git a/fusion_plating/fusion_plating_jobs/models/fp_notification_trigger.py b/fusion_plating/fusion_plating_jobs/models/fp_notification_trigger.py new file mode 100644 index 00000000..265ce4dd --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_notification_trigger.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# +# Adds 'job_confirmed' and 'job_complete' trigger events to the +# fp.notification.template selection. Fired from fp.job lifecycle +# hooks (action_confirm, button_mark_done). +# +# bridge_mrp's existing 'mo_confirmed' / 'mo_complete' triggers +# stay alive for the legacy MO flow. + +from odoo import fields, models + + +class FpNotificationTemplate(models.Model): + _inherit = 'fp.notification.template' + + trigger_event = fields.Selection( + selection_add=[ + ('job_confirmed', 'Plating Job Confirmed'), + ('job_complete', 'Plating Job Complete'), + ], + ondelete={ + 'job_confirmed': 'cascade', + 'job_complete': 'cascade', + }, + ) diff --git a/fusion_plating/fusion_plating_jobs/models/fusion_plating_kpi_value.py b/fusion_plating/fusion_plating_jobs/models/fusion_plating_kpi_value.py new file mode 100644 index 00000000..4ac1a4d9 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fusion_plating_kpi_value.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# +# Tags KPI values by source: 'mrp' (legacy bridge_mrp rollups) vs +# 'jobs' (native fp.job rollups). Lets Phase 9 / Phase 10 dashboards +# show both side-by-side or filter to one. + +from odoo import fields, models + + +class FusionPlatingKpiValue(models.Model): + _inherit = 'fusion.plating.kpi.value' + + x_fc_source = fields.Selection( + [ + ('mrp', 'MRP (legacy)'), + ('jobs', 'Native Jobs'), + ], + string='Data Source', + default='mrp', + index=True, + help='Which data path produced this KPI value. Phase 9+ ' + 'rollups from fp.job/fp.job.step set this to jobs.', + ) 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 e5e8af1a..1324a3c9 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 @@ -462,3 +462,44 @@ class TestPhase3Refactors(TransactionCase): [('x_fc_job_id', '=', job.id)], ) self.assertFalse(inspections) + + +class TestPhase4Refactors(TransactionCase): + """Phase 4 — light refactors batch B (notifications, KPI source tag). + + Configurator integration is already covered by Task 2.5's SO confirm + hook (which reads x_fc_part_catalog_id / x_fc_coating_config_id from + sale.order.line — see TestSoConfirmHook above). + """ + + 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_kpi_value_has_source_field(self): + if 'fusion.plating.kpi.value' in self.env: + self.assertIn( + 'x_fc_source', + self.env['fusion.plating.kpi.value']._fields, + ) + + def test_notification_template_has_job_triggers(self): + if 'fp.notification.template' in self.env: + triggers = dict( + self.env['fp.notification.template'] + ._fields['trigger_event'].selection + ) + self.assertIn('job_confirmed', triggers) + self.assertIn('job_complete', triggers) + + def test_action_confirm_calls_fire_notification(self): + # Smoke test — creates a job, confirms it, verifies no exception + # thrown by the notification path even when no templates exist. + job = self.env['fp.job'].create({ + 'partner_id': self.partner.id, + 'product_id': self.product.id, + 'qty': 1.0, + }) + job.action_confirm() # Should not raise even with no templates + self.assertEqual(job.state, 'confirmed')