Five new boolean flags on res.partner applied to CHILD contacts: x_fc_receives_certs, _qc, _quotes_so, _invoices, and _is_global_contact. Single resolver helper res.partner._fp_resolve_notification_recipients (stream, delivery_location=None) walks location contacts first then company contacts, returning emails for contacts that opted into the stream (or flagged themselves global). Falls back to partner.email when no contact opts in so existing customers keep their exact pre-Sub-6 routing. fp.notification.template._dispatch now maps each trigger event to a stream (so_confirmed→quotes_so, invoice_posted→invoices, shipped→ certs, etc.) and overrides the mail_template's email_to with the resolved list. fp.delivery passes its delivery_address_id so the shipped/CoC email routes through location-scoped contacts when they exist. Partner form gets a new "Communication Routing" tab on child contact forms with the 5 flags (hides the per-stream checkboxes when Global Contact is on, since it overrides them). fusion_plating_certificates → 19.0.4.0.0 (adds the flag fields) fusion_plating_notifications → 19.0.5.0.0 (+depends certificates) Smoke on entech: 11/11 assertions pass including per-stream routing, delivery-location scoping, zero-flag fallback, email-less skip, unknown-stream + global behaviour, and case-insensitive dedup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
3.2 KiB
Python
81 lines
3.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
#
|
|
# Sub 6 — Recipient resolver.
|
|
#
|
|
# Every outbound-notification path now routes its recipient lookup
|
|
# through `_fp_resolve_notification_recipients`. The helper consults
|
|
# child contacts (and optionally a delivery-location partner's child
|
|
# contacts) for per-stream flags, falling back to the company's own
|
|
# `email` when no contact opts in. This preserves the pre-Sub-6
|
|
# behaviour exactly for customers who haven't configured any flags.
|
|
|
|
from odoo import models
|
|
|
|
|
|
FP_STREAMS = ('certs', 'qc', 'quotes_so', 'invoices')
|
|
|
|
|
|
class ResPartner(models.Model):
|
|
_inherit = 'res.partner'
|
|
|
|
def _fp_resolve_notification_recipients(self, stream, delivery_location=None):
|
|
"""Return a list of email addresses that should receive a given
|
|
notification stream for this customer.
|
|
|
|
Args:
|
|
stream: one of 'certs', 'qc', 'quotes_so', 'invoices'. Any other
|
|
value returns the bare fallback list.
|
|
delivery_location: optional res.partner (typically with type=
|
|
'delivery') whose own child contacts are
|
|
consulted first, at the same priority as the
|
|
company-level contacts.
|
|
|
|
Fallback: if no contact at either level carries a matching flag
|
|
(or the global flag), the result is the company partner's own
|
|
email. This makes the resolver drop-in safe — no customer ever
|
|
silently stops receiving notifications after Sub 6 ships.
|
|
"""
|
|
self.ensure_one()
|
|
recipients = []
|
|
|
|
# Gather candidate contact recordsets: location-scoped first, then
|
|
# company-scoped. Duplicates are dropped by the final dedup pass.
|
|
candidate_sets = []
|
|
if delivery_location and delivery_location != self:
|
|
candidate_sets.append(delivery_location.child_ids)
|
|
candidate_sets.append(self.child_ids)
|
|
|
|
flag_name = f'x_fc_receives_{stream}' if stream in FP_STREAMS else None
|
|
for contacts in candidate_sets:
|
|
for contact in contacts:
|
|
if not contact.email:
|
|
continue
|
|
is_global = getattr(contact, 'x_fc_is_global_contact', False)
|
|
matches_stream = (
|
|
flag_name is not None
|
|
and getattr(contact, flag_name, False)
|
|
)
|
|
if is_global or matches_stream:
|
|
recipients.append(contact.email)
|
|
|
|
if not recipients:
|
|
# Nobody opted in via contacts — fall back to the company
|
|
# email (and the location's email, if distinct).
|
|
if delivery_location and delivery_location != self and delivery_location.email:
|
|
recipients.append(delivery_location.email)
|
|
if self.email:
|
|
recipients.append(self.email)
|
|
|
|
# Case-insensitive dedup, preserve first-seen order.
|
|
seen = set()
|
|
unique = []
|
|
for email in recipients:
|
|
key = email.strip().lower()
|
|
if key and key not in seen:
|
|
seen.add(key)
|
|
unique.append(email)
|
|
return unique
|