Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-04-22-sub6-contact-profiles-design.md
gsinghpal f9e1b62409 docs(notifications): Sub 6 design spec — contact profiles + routing
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>
2026-04-22 23:54:59 -04:00

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:

  1. Per-contact permission flags — child contacts under res.partner gain five booleans that control which communication streams they receive:
    • x_fc_receives_certs — CoCs, thickness reports
    • x_fc_receives_qc — NCR / CAPA / quality hold / contract review
    • x_fc_receives_quotes_so — quote sends, SO acks, order confirmations
    • x_fc_receives_invoices — invoices, payment receipts, dunning
    • x_fc_is_global_contact — firehose; every stream
  2. Multiple delivery locations via Odoo native child_ids with type='delivery'. Each delivery-location partner can carry its own child contacts with the five flags scoped to that location.
  3. Recipient resolver — single helper res.partner._fp_resolve_notification_recipients that every dispatch path uses.

Out of scope

  • Splitting the partner-level x_fc_send_coc / x_fc_send_thickness_report flags 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 type selection on res.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_contact OR x_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_requirementunchanged.

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

  1. 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.
  2. Case-insensitive dedup — prevents double-emailing a contact who appears at both the location and company level.
  3. Stream name registry_FP_STREAMS constant at the top of the partner module. Adding a future stream means one edit + one event-map addition.
  4. Email must be truthy — contacts without an email are silently skipped, not errors.
  5. 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) →
    • certs with location → {dan, alice, carol}
    • certs without location → {alice, carol}

7.2 Dispatcher integration

  • Post an invoice → assert fp.notification.log records 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:

  1. fusion_plating_certificates (new flag fields)
  2. 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.