feat(certificates): cert Customer Contact is multi-contact, auto-filled, sends to all

fp.certificate.contact_partner_id (single 'Customer Contact') becomes
contact_partner_ids (Many2many) — same shape as the partner's Default CoC
Contacts, as requested.

- Auto-populate: at job creation (fp.job cert resolution) + lazy-fill at issue,
  contact_partner_ids = the customer's x_fc_default_coc_contact_ids (ALL).
- Send: action_send_to_customer pre-fills the composer with exactly the cert's
  contact_partner_ids, so the CoC goes to all the defined clients (fallback:
  company).
- Primary: the FIRST contact prints on the CoC + is gated for email; report
  uses contact_partner_ids[:1].
- Gate: requires >=1 Customer Contact + the primary has an email.
- View: many2many_tags.
- Migration 19.0.10.3.0: copies each cert's old single contact into the new M2m,
  drops the orphaned column.

Deployed + verified on entech: migration copied 16 certs, old column dropped,
field is M2m, send pre-fills the cert contacts, CoC report renders. entech-only
part_line_ids preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 18:09:36 -04:00
parent ba6aeaaca9
commit 6f006e24ad
9 changed files with 99 additions and 40 deletions

View File

@@ -2678,7 +2678,7 @@ class FpJob(models.Model):
(a per-spec override). Left empty
otherwise so the LIVE company owner
resolves at render / issue time.
- contact_partner_id ← partner.x_fc_default_coc_contact_ids[:1]
- contact_partner_ids ← partner.x_fc_default_coc_contact_ids (all)
- nc_quantity ← qty_scrapped + qty_visual_insp_rejects
Honours part.certificate_requirement (coc / coc_thickness /
@@ -2714,9 +2714,10 @@ class FpJob(models.Model):
# Contact: per-customer default; blank means manager picks at issue.
contact = False
if 'x_fc_default_coc_contact_ids' in self.partner_id._fields:
# Primary = first of the customer's CoC contacts; the rest are
# CC'd at send (fp.certificate.action_send_to_customer).
contact = self.partner_id.x_fc_default_coc_contact_ids[:1]
# ALL the customer's CoC contacts -> the cert's Customer Contact
# M2m. First is the primary (printed on the CoC); every contact
# is emailed at send (fp.certificate.action_send_to_customer).
contact = self.partner_id.x_fc_default_coc_contact_ids
# NC qty: scrapped + visual rejects. Both NULL-safe.
nc_qty = int(
(self.qty_scrapped or 0)
@@ -2786,8 +2787,8 @@ class FpJob(models.Model):
vals['process_description'] = recipe.name or ''
if 'certified_by_id' in Cert._fields and signer:
vals['certified_by_id'] = signer.id
if 'contact_partner_id' in Cert._fields and contact:
vals['contact_partner_id'] = contact.id
if 'contact_partner_ids' in Cert._fields and contact:
vals['contact_partner_ids'] = [(6, 0, contact.ids)]
if 'entech_wo_number' in Cert._fields:
vals['entech_wo_number'] = self.name or ''
cert = Cert.create(vals)