feat(notifications): cert authority events + QM activity (Tasks 17-20)

TRIGGER_EVENTS extended with three new events:
  - cert_awaiting_issuance — fires on in_progress → awaiting_cert
  - cert_voided_re_notify  — fires on awaiting_ship → awaiting_cert
                             regress (cert voided post-issue)
  - job_shipped            — fires on button_mark_shipped

_dispatch routes cert events through new internal-recipient resolver
(QM/Manager/Owner via all_group_ids, transitive per Rule 13l)
instead of the partner-based stream lookup. Other events unchanged.

Mail templates (fp_cert_authority_templates.xml): two new
mail.template records bound to fp.job. Amber accent bar for awaiting,
red accent bar for void-re-issue. Deep-link to
/odoo/action-...?tab=certificates so QM lands on the right tab.

Activity type (fp_activity_types_data.xml): mail.activity.type
activity_type_issue_coc — bound to fp.job, 1-day delay, certificate
icon.

fp.job helpers:
  _fp_schedule_cert_activity: round-robin by oldest login_date,
    idempotent on existing open activity, soft-fails if helpers
    are missing.
  _fp_resolve_cert_activities: auto-resolves on awaiting_ship,
    soft-fails on per-activity exceptions.

Manifest bumps:
  fusion_plating_notifications 19.0.6.6.1 → 19.0.7.0.0
  fusion_plating_jobs: data list gains fp_activity_types_data.xml

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-25 09:53:09 -04:00
parent c00831a72a
commit 26a1086623
6 changed files with 272 additions and 3 deletions

View File

@@ -2390,6 +2390,78 @@ class FpJob(models.Model):
if hasattr(job, '_fp_schedule_cert_activity'):
job._fp_schedule_cert_activity()
def _fp_schedule_cert_activity(self):
"""Schedule an Issue-CoC mail.activity for one QM. Round-robin
by oldest login_date (least recently active QM, likely least
busy). Idempotent — re-firing while an open activity already
exists is a no-op.
Spec 2026-05-25 §mail.activity belt + suspenders.
"""
self.ensure_one()
activity_type = self.env.ref(
'fusion_plating_jobs.activity_type_issue_coc',
raise_if_not_found=False,
)
if not activity_type:
return
# Idempotency: skip if an open activity of this type exists.
existing = self.activity_ids.filtered(
lambda a: a.activity_type_id == activity_type
)
if existing:
return
Template = self.env.get('fp.notification.template')
if not Template or not hasattr(
Template, '_fp_resolve_cert_authority_users'):
return
qms = Template.sudo()._fp_resolve_cert_authority_users(self)
if not qms:
return
# Round-robin: pick the QM who logged in least recently (likely
# least busy). NULL login_date sorts first.
qm = qms.sorted(
lambda u: u.login_date or fields.Datetime.from_string(
'1970-01-01 00:00:00'
)
)[:1]
try:
self.activity_schedule(
activity_type_id=activity_type.id,
user_id=qm.id,
summary=_('Issue CoC for %s') % (
self.display_wo_name or self.name or 'job'
),
)
except Exception as e:
_logger.warning(
"Job %s: schedule cert activity failed: %s", self.name, e,
)
def _fp_resolve_cert_activities(self):
"""Auto-resolve all open Issue-CoC activities on this job.
Called from _fp_check_advance_after_cert_issue when the job
transitions awaiting_cert → awaiting_ship. Spec 2026-05-25.
"""
self.ensure_one()
activity_type = self.env.ref(
'fusion_plating_jobs.activity_type_issue_coc',
raise_if_not_found=False,
)
if not activity_type:
return
open_activities = self.activity_ids.filtered(
lambda a: a.activity_type_id == activity_type
)
for act in open_activities:
try:
act.action_feedback(feedback=_('Cert issued — auto-resolved.'))
except Exception as e:
_logger.warning(
"Job %s: auto-resolve cert activity failed: %s",
self.name, e,
)
def _fp_create_certificates(self):
"""Auto-create one draft fp.certificate per type returned by
_resolve_required_cert_types. Idempotent per type — re-running