feat(notifications): Sub 6 — contact profiles + communication routing
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>
This commit is contained in:
128
fusion_plating/docs/superpowers/tests/2026-04-22-sub6-smoke.py
Normal file
128
fusion_plating/docs/superpowers/tests/2026-04-22-sub6-smoke.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Sub 6 smoke test — runs inside odoo-shell on entech."""
|
||||
env = env
|
||||
Partner = env['res.partner']
|
||||
|
||||
# ---- Sanity: flags + resolver exist ----------------------------------
|
||||
for f in ('x_fc_receives_certs', 'x_fc_receives_qc', 'x_fc_receives_quotes_so',
|
||||
'x_fc_receives_invoices', 'x_fc_is_global_contact'):
|
||||
assert f in Partner._fields, f'missing flag {f}'
|
||||
assert hasattr(Partner, '_fp_resolve_notification_recipients')
|
||||
print('[OK] Flags + resolver helper present')
|
||||
|
||||
# ---- Fresh company + 4 contacts --------------------------------------
|
||||
company = Partner.create({
|
||||
'name': 'Sub 6 Smoke Co.',
|
||||
'is_company': True,
|
||||
'customer_rank': 1,
|
||||
'email': 'company@acme.com',
|
||||
})
|
||||
|
||||
alice = Partner.create({
|
||||
'name': 'Alice — Certs + QC',
|
||||
'parent_id': company.id,
|
||||
'email': 'alice@acme.com',
|
||||
'x_fc_receives_certs': True,
|
||||
'x_fc_receives_qc': True,
|
||||
})
|
||||
bob = Partner.create({
|
||||
'name': 'Bob — Invoices',
|
||||
'parent_id': company.id,
|
||||
'email': 'bob@acme.com',
|
||||
'x_fc_receives_invoices': True,
|
||||
})
|
||||
carol = Partner.create({
|
||||
'name': 'Carol — Global',
|
||||
'parent_id': company.id,
|
||||
'email': 'carol@acme.com',
|
||||
'x_fc_is_global_contact': True,
|
||||
})
|
||||
dave = Partner.create({
|
||||
'name': 'Dave — no flags (silent)',
|
||||
'parent_id': company.id,
|
||||
'email': 'dave@acme.com',
|
||||
})
|
||||
print('[OK] Company + 4 contacts created')
|
||||
|
||||
# ---- Stream resolution ------------------------------------------------
|
||||
certs = set(e.lower() for e in company._fp_resolve_notification_recipients('certs'))
|
||||
assert certs == {'alice@acme.com', 'carol@acme.com'}, f'certs got {certs}'
|
||||
print(f'[OK] certs stream → {sorted(certs)}')
|
||||
|
||||
invoices = set(e.lower() for e in company._fp_resolve_notification_recipients('invoices'))
|
||||
assert invoices == {'bob@acme.com', 'carol@acme.com'}, f'invoices got {invoices}'
|
||||
print(f'[OK] invoices stream → {sorted(invoices)}')
|
||||
|
||||
qc = set(e.lower() for e in company._fp_resolve_notification_recipients('qc'))
|
||||
assert qc == {'alice@acme.com', 'carol@acme.com'}, f'qc got {qc}'
|
||||
print(f'[OK] qc stream → {sorted(qc)}')
|
||||
|
||||
quotes = set(e.lower() for e in company._fp_resolve_notification_recipients('quotes_so'))
|
||||
assert quotes == {'carol@acme.com'}, f'quotes_so got {quotes}'
|
||||
print(f'[OK] quotes_so stream → {sorted(quotes)}')
|
||||
|
||||
# ---- Delivery-location scoping ---------------------------------------
|
||||
location = Partner.create({
|
||||
'name': 'Sub 6 Smoke Warehouse',
|
||||
'parent_id': company.id,
|
||||
'type': 'delivery',
|
||||
'email': 'warehouse@acme.com',
|
||||
})
|
||||
dan = Partner.create({
|
||||
'name': 'Dan — Warehouse Certs',
|
||||
'parent_id': location.id,
|
||||
'email': 'dan@warehouse.com',
|
||||
'x_fc_receives_certs': True,
|
||||
})
|
||||
certs_loc = set(e.lower() for e in company._fp_resolve_notification_recipients(
|
||||
'certs', delivery_location=location,
|
||||
))
|
||||
assert 'dan@warehouse.com' in certs_loc
|
||||
assert 'alice@acme.com' in certs_loc
|
||||
assert 'carol@acme.com' in certs_loc
|
||||
print(f'[OK] certs+location → {sorted(certs_loc)}')
|
||||
|
||||
# ---- Backward compat: no flags → falls back to company email --------
|
||||
clean_company = Partner.create({
|
||||
'name': 'Legacy Customer',
|
||||
'is_company': True,
|
||||
'customer_rank': 1,
|
||||
'email': 'legacy@customer.com',
|
||||
})
|
||||
fallback = clean_company._fp_resolve_notification_recipients('certs')
|
||||
assert fallback == ['legacy@customer.com'], f'expected fallback, got {fallback}'
|
||||
print(f'[OK] Fallback (no contacts) → {fallback}')
|
||||
|
||||
# ---- Contact with no email gets skipped -----------------------------
|
||||
eve = Partner.create({
|
||||
'name': 'Eve — no email',
|
||||
'parent_id': company.id,
|
||||
'x_fc_receives_certs': True,
|
||||
# intentionally no email
|
||||
})
|
||||
certs2 = set(e.lower() for e in company._fp_resolve_notification_recipients('certs'))
|
||||
# Should still be alice + carol; Eve skipped
|
||||
assert certs2 == {'alice@acme.com', 'carol@acme.com'}
|
||||
print('[OK] Email-less contact silently skipped')
|
||||
|
||||
# ---- Unknown stream → fallback ---------------------------------------
|
||||
unknown = company._fp_resolve_notification_recipients('bogus_stream')
|
||||
# Should return just carol (global) since global applies to all; if no global,
|
||||
# falls back to company email. Carol is global so she should still match.
|
||||
assert 'carol@acme.com' in [e.lower() for e in unknown]
|
||||
print(f'[OK] Unknown stream with global contact → {unknown}')
|
||||
|
||||
# ---- Case-insensitive dedup ------------------------------------------
|
||||
# Add a contact with a duplicate casing
|
||||
duplicate = Partner.create({
|
||||
'name': 'Alice dup',
|
||||
'parent_id': company.id,
|
||||
'email': 'ALICE@acme.com', # different case
|
||||
'x_fc_receives_certs': True,
|
||||
})
|
||||
certs_dedup = company._fp_resolve_notification_recipients('certs')
|
||||
lowered = [e.lower() for e in certs_dedup]
|
||||
assert lowered.count('alice@acme.com') == 1, f'dedup failed, got {certs_dedup}'
|
||||
print('[OK] Case-insensitive dedup')
|
||||
|
||||
env.cr.rollback()
|
||||
print('\n=== SUB 6 SMOKE PASS — all assertions held ===')
|
||||
Reference in New Issue
Block a user