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>
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
# 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`
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
# 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:
|
||||
```python
|
||||
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:
|
||||
```python
|
||||
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:
|
||||
```xml
|
||||
<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.*
|
||||
Reference in New Issue
Block a user