From f9e1b6240907f769f64881e7acada958d8bb79e9 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 22 Apr 2026 23:54:59 -0400 Subject: [PATCH] =?UTF-8?q?docs(notifications):=20Sub=206=20design=20spec?= =?UTF-8?q?=20=E2=80=94=20contact=20profiles=20+=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...2026-04-22-sub6-contact-profiles-design.md | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 fusion_plating/docs/superpowers/specs/2026-04-22-sub6-contact-profiles-design.md diff --git a/fusion_plating/docs/superpowers/specs/2026-04-22-sub6-contact-profiles-design.md b/fusion_plating/docs/superpowers/specs/2026-04-22-sub6-contact-profiles-design.md new file mode 100644 index 00000000..a45b64dd --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-04-22-sub6-contact-profiles-design.md @@ -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_` 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 + + + + + +``` + +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.*