feat(jobs): Phase 4 light refactors — notifications, KPI source tag
- Adds 'job_confirmed' and 'job_complete' trigger events to fp.notification.template (legacy 'mo_confirmed' / 'mo_complete' stay for bridge_mrp). - fp.job.action_confirm and button_mark_done now fire those notifications best-effort via fp.notification.template._dispatch (silent skip if templates absent or notifications module missing). - Adds x_fc_source ['mrp', 'jobs'] tag to fusion.plating.kpi.value so Phase 9 dashboards can filter or display both sources. - Verified aerospace/nuclear/cgp/safety modules don't directly reference mrp.production or mrp.workorder. Configurator integration was already covered by Task 2.5's SO confirm hook (reads x_fc_part_catalog_id and x_fc_coating_config_id from sale.order.line). Manifest 19.0.1.6.0 -> 19.0.1.7.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:
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Native Jobs',
|
'name': 'Fusion Plating — Native Jobs',
|
||||||
'version': '19.0.1.6.0',
|
'version': '19.0.1.7.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||||
'description': """
|
'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_batch', # fusion.plating.batch (Phase 3)
|
||||||
'fusion_plating_certificates', # fp.certificate, fp.thickness.reading
|
'fusion_plating_certificates', # fp.certificate, fp.thickness.reading
|
||||||
'fusion_plating_configurator', # fp.part.catalog, fp.coating.config
|
'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_logistics', # fusion.plating.delivery
|
||||||
|
'fusion_plating_notifications', # fp.notification.template (Phase 4)
|
||||||
'fusion_plating_portal', # fusion.plating.portal.job
|
'fusion_plating_portal', # fusion.plating.portal.job
|
||||||
'fusion_plating_quality', # fusion.plating.customer.spec, fusion.plating.quality.hold
|
'fusion_plating_quality', # fusion.plating.customer.spec, fusion.plating.quality.hold
|
||||||
'fusion_plating_receiving', # fp.racking.inspection (Phase 3)
|
'fusion_plating_receiving', # fp.racking.inspection (Phase 3)
|
||||||
|
|||||||
@@ -19,3 +19,7 @@ from . import fp_certificate
|
|||||||
from . import fp_thickness_reading
|
from . import fp_thickness_reading
|
||||||
from . import fp_delivery
|
from . import fp_delivery
|
||||||
from . import fp_racking_inspection
|
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
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ class FpJob(models.Model):
|
|||||||
job._fp_create_portal_job()
|
job._fp_create_portal_job()
|
||||||
job._fp_create_qc_check_if_needed()
|
job._fp_create_qc_check_if_needed()
|
||||||
job._fp_create_racking_inspection()
|
job._fp_create_racking_inspection()
|
||||||
|
job._fp_fire_notification('job_confirmed')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _fp_create_racking_inspection(self):
|
def _fp_create_racking_inspection(self):
|
||||||
@@ -382,8 +383,43 @@ class FpJob(models.Model):
|
|||||||
job.date_finished = fields.Datetime.now()
|
job.date_finished = fields.Datetime.now()
|
||||||
job._fp_create_delivery()
|
job._fp_create_delivery()
|
||||||
job._fp_create_certificates()
|
job._fp_create_certificates()
|
||||||
|
job._fp_fire_notification('job_complete')
|
||||||
return True
|
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):
|
def _fp_create_delivery(self):
|
||||||
"""Create a draft fusion.plating.delivery linked to this job."""
|
"""Create a draft fusion.plating.delivery linked to this job."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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.',
|
||||||
|
)
|
||||||
@@ -462,3 +462,44 @@ class TestPhase3Refactors(TransactionCase):
|
|||||||
[('x_fc_job_id', '=', job.id)],
|
[('x_fc_job_id', '=', job.id)],
|
||||||
)
|
)
|
||||||
self.assertFalse(inspections)
|
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')
|
||||||
|
|||||||
Reference in New Issue
Block a user