Five per-contact boolean flags (certs / qc / quotes-so / invoices / global), native Odoo delivery-location child contacts reused for per-location routing, and a single resolver on res.partner that the dispatcher + all mail-send sites call. Fallback to self.email keeps existing customers bit-identical when no flags are set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
Sub 6 — Contact Profiles & Communication Routing
Date: 2026-04-22
Module scope: fusion_plating_notifications (owns the recipient resolver + dispatcher
wiring), fusion_plating_certificates (owns the contact-level permission fields on
res.partner).
Status: Design approved; implementing in this session.
Predecessor context: Fine-Tuning Initiative, entry in fusion_plating/CLAUDE.md (Sub 6
preview from client transcripts A/B/C). Builds on Sub 2's _fp_resolve_cert_requirement.
1. Scope
Three pieces, shipped together:
- Per-contact permission flags — child contacts under
res.partnergain five booleans that control which communication streams they receive:x_fc_receives_certs— CoCs, thickness reportsx_fc_receives_qc— NCR / CAPA / quality hold / contract reviewx_fc_receives_quotes_so— quote sends, SO acks, order confirmationsx_fc_receives_invoices— invoices, payment receipts, dunningx_fc_is_global_contact— firehose; every stream
- Multiple delivery locations via Odoo native
child_idswithtype='delivery'. Each delivery-location partner can carry its own child contacts with the five flags scoped to that location. - Recipient resolver — single helper
res.partner._fp_resolve_notification_recipientsthat every dispatch path uses.
Out of scope
- Splitting the partner-level
x_fc_send_coc/x_fc_send_thickness_reportflags into per- contact versions. Those stay as the "generate?" decision; the new flags handle "who receives?". Orthogonal. - Rewriting
_fp_resolve_cert_requirement. It keeps returning generation booleans; the new recipient resolver sits alongside it. - Portal user access — contact flags are purely for outbound email routing, not portal login permissions.
- Reworking Odoo's native
typeselection onres.partner(contact / invoice / delivery / other). Sub 6 adds data alongside; doesn't touch the selection.
2. Data Model
2.1 res.partner additions — in fusion_plating_certificates/models/res_partner.py
x_fc_receives_certs = fields.Boolean(
string='Receives Certificates (CoC, thickness)',
default=False, tracking=True,
help='Contact receives certificate PDFs and quality documents when a job ships.',
)
x_fc_receives_qc = fields.Boolean(
string='Receives QC Alerts',
default=False, tracking=True,
help='Contact receives NCR / CAPA / quality hold / contract review notifications.',
)
x_fc_receives_quotes_so = fields.Boolean(
string='Receives Quotes & Sales Orders',
default=False, tracking=True,
help='Contact receives quotes, sale order acknowledgements, and order confirmations.',
)
x_fc_receives_invoices = fields.Boolean(
string='Receives Invoices',
default=False, tracking=True,
help='Contact receives invoices, payment receipts, and dunning communications.',
)
x_fc_is_global_contact = fields.Boolean(
string='Global Contact (receives everything)',
default=False, tracking=True,
help='When set, this contact receives every outbound stream regardless of the '
'per-stream flags above. Typical use: a primary account-manager contact '
'at the customer who wants full visibility.',
)
All five default False so existing customers see no routing change until the admin
explicitly ticks a flag.
2.2 No new models
Delivery locations already supported via Odoo's native res.partner.type = 'delivery' child
partners. No custom "location" model needed.
3. Recipient Resolver
3.1 Helper
# res.partner (in fusion_plating_notifications)
_FP_STREAMS = ('certs', 'qc', 'quotes_so', 'invoices')
def _fp_resolve_notification_recipients(self, stream, delivery_location=None):
"""Return a list of email addresses for the given stream.
Args:
stream: one of 'certs', 'qc', 'quotes_so', 'invoices'
delivery_location: optional res.partner with type='delivery'; if given,
its child contacts are consulted FIRST with a higher
priority than the company-level contacts.
Fallback behaviour (critical for backward compatibility):
If neither the company nor the optional location has any child contacts
flagged for this stream (or as global), fall back to `self.email`.
This preserves the pre-Sub-6 behaviour where every dispatch used the
partner's top-level email.
"""
3.2 Selection logic
For each contact, in both the delivery_location.child_ids and the company's own
child_ids:
- Skip contacts with no
email. - Include if
x_fc_is_global_contactORx_fc_receives_<stream>is True.
Dedup emails (case-insensitive) before returning.
If zero contacts matched across BOTH levels, fall back to self.email (and the delivery-
location's email if provided). Never return an empty list without trying fallbacks.
3.3 Stream mapping (event → stream)
Wired in fp.notification.template._dispatch:
| Trigger event | Stream |
|---|---|
so_confirmed, quote_sent, order_acknowledged |
quotes_so |
invoice_posted, payment_received, dunning_sent |
invoices |
shipped, coc_issued, thickness_report_sent |
certs |
ncr_opened, capa_opened, quality_hold_opened, contract_review_signed |
qc |
Unknown event → fall back to self.email (preserves current behaviour).
4. Integration Points
4.1 fp.notification.template._dispatch (or equivalent)
Replace every partner.email recipient lookup with:
recipients = partner._fp_resolve_notification_recipients(
stream=_FP_EVENT_TO_STREAM.get(event, None),
delivery_location=context.get('delivery_location'),
)
4.2 Cert-email send (on MO done)
In fusion_plating_bridge_mrp/models/mrp_production.py, the _fp_generate_cert_pdf path
posts an email via mail.mail. Update its recipient:
to_emails = partner._fp_resolve_notification_recipients(
'certs', delivery_location=delivery.delivery_address_id,
)
4.3 Delivery shipped email (fp.delivery.action_mark_delivered)
Same pattern — stream 'certs' + the delivery's own address as delivery_location.
4.4 _fp_resolve_cert_requirement — unchanged.
Sub 2's cert resolver is about REQUIREMENT ("should we generate a CoC?"), not recipient ("who gets the email?"). Keeping it untouched avoids breaking downstream callers. The recipient decision is handled by the new resolver in parallel.
5. Views
5.1 Partner form — extend the Contacts tab
Add five boolean columns to the child-contact inline list:
<field name="x_fc_receives_certs" widget="boolean_toggle" optional="show"/>
<field name="x_fc_receives_qc" widget="boolean_toggle" optional="show"/>
<field name="x_fc_receives_quotes_so" widget="boolean_toggle" optional="show"/>
<field name="x_fc_receives_invoices" widget="boolean_toggle" optional="show"/>
<field name="x_fc_is_global_contact" widget="boolean_toggle" optional="show"/>
Inherit on base.view_partner_form targeting the child_ids sub-tree.
5.2 Company partner form — "Notification Routing" block
On the main partner form (when is_company=True), show a read-only summary block
under the existing "Plating Documents" tab:
- Effective recipient count per stream (computed live from child contacts)
- A short help note: "Leave all contacts empty to keep the current 'email the company partner' behaviour."
5.3 Delivery-location partner form
Same Contacts tab inherit applies — delivery-location partners are res.partner records, so
the same view inherit surfaces the flag columns automatically.
6. Defensive Measures
- Fallback to
self.email— if every level returns nothing, the resolver still returns the company's email. No customer ever silently stops receiving notifications after upgrade. - Case-insensitive dedup — prevents double-emailing a contact who appears at both the location and company level.
- Stream name registry —
_FP_STREAMSconstant at the top of the partner module. Adding a future stream means one edit + one event-map addition. - Email must be truthy — contacts without an email are silently skipped, not errors.
- No changes to cert resolver — Sub 2's helper stays the single source of truth for "what gets generated". Sub 6's resolver owns "who receives what gets generated".
7. Testing
7.1 Recipient resolver smoke
- Partner with no child contacts,
email='acct@customer.com'→ resolver returns['acct@customer.com']for every stream. - Partner with 3 contacts: Alice (certs+qc), Bob (invoices), Carol (global) →
certs→ {alice, carol}invoices→ {bob, carol}quotes_so→ {carol}
- Delivery-location partner with its own child contact Dan (certs) →
certswith location → {dan, alice, carol}certswithout location → {alice, carol}
7.2 Dispatcher integration
- Post an invoice → assert
fp.notification.logrecords list Bob + Carol, not the company partner. - Mark a delivery shipped → assert CoC email went to Dan + Alice + Carol.
7.3 Backward compatibility
- Upgrade a DB with zero contact flags set → every trigger event still delivers to
partner.email. No regression.
8. File Manifest
fusion_plating_certificates/
├── __manifest__.py (version bump)
├── models/
│ └── res_partner.py (+5 boolean flags)
└── views/
└── res_partner_views.xml (inherit Contacts tab, add columns)
fusion_plating_notifications/
├── __manifest__.py (version bump)
├── models/
│ ├── __init__.py (+ res_partner)
│ ├── res_partner.py NEW — resolver helper
│ ├── fp_notification_template.py (dispatcher wires resolver)
│ ├── account_move.py (pass stream='invoices')
│ ├── account_payment.py (pass stream='invoices')
│ └── fp_delivery.py (pass stream='certs' + location)
└── views/
└── res_partner_views.xml NEW — routing summary block
Rough LOC: ~250 Python, ~100 XML.
9. Rollout
Update order on entech:
fusion_plating_certificates(new flag fields)fusion_plating_notifications(resolver + dispatcher wiring)
Post-deploy verification:
- Open any customer → Contacts tab shows 5 new columns
- Create a child contact with
x_fc_receives_invoices=True, post an invoice → log shows the child's email - Clear all flags on that customer → next post goes back to company
.email
End of spec.