feat(certificates): multiple Default CoC Contacts per customer (M2o -> M2m)

res.partner.x_fc_default_coc_contact_id (single Many2one) becomes
x_fc_default_coc_contact_ids (self-referential Many2many 'Default CoC
Contacts') so a customer can list several contacts who need the CoC.

- res.partner: M2m field (rel fp_default_coc_contact_rel) + many2many_tags.
- Cert: contact_partner_id (primary addressee printed on the cert) is set to
  the FIRST CoC contact at job creation + lazy-filled at issue.
- Send: action_send_to_customer pre-fills the email composer with ALL the
  customer's CoC contacts (primary + the rest), falling back to the company.
- fp.job cert-default resolution + the action_issue gate wording updated.
- Migration 19.0.10.2.0: copies each partner's old single value into the new
  M2m, then drops the orphaned column.

Deployed + verified on entech: migration copied 2 existing values, old column
dropped, field is M2m, send pre-fills all contacts. entech-only part_line_ids
/ multi-part resolver preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 17:31:17 -04:00
parent dcd4955bb7
commit ba6aeaaca9
7 changed files with 108 additions and 28 deletions

View File

@@ -521,10 +521,12 @@ class FpCertificate(models.Model):
# effect retroactively at issue time.
if (not rec.contact_partner_id
and rec.partner_id
and 'x_fc_default_coc_contact_id' in rec.partner_id._fields
and rec.partner_id.x_fc_default_coc_contact_id):
and 'x_fc_default_coc_contact_ids' in rec.partner_id._fields
and rec.partner_id.x_fc_default_coc_contact_ids):
# Primary = first of the customer's CoC contacts; the rest
# are CC'd at send (action_send_to_customer).
rec.contact_partner_id = (
rec.partner_id.x_fc_default_coc_contact_id
rec.partner_id.x_fc_default_coc_contact_ids[:1]
)
# Lazy-fill the signer from the LIVE company owner (Settings
# "Certificate Owner") when no per-cert / per-spec signer was
@@ -578,13 +580,13 @@ class FpCertificate(models.Model):
'(Settings > Fusion Plating).'
) % {'name': rec.name or rec.display_name})
# Customer contact — the named recipient printed on the
# cert and emailed when it ships. Auto-filled from
# partner.x_fc_default_coc_contact_id when set.
# cert and emailed when it ships. Auto-filled from the FIRST
# of partner.x_fc_default_coc_contact_ids when set.
if not rec.contact_partner_id:
raise UserError(_(
'Cannot issue certificate "%(name)s" — Customer '
'Contact is not set.\n\nPick the recipient contact, '
'or configure a Default CoC Contact on customer '
'or configure Default CoC Contacts on customer '
'"%(cust)s".'
) % {
'name': rec.name or rec.display_name,
@@ -1114,11 +1116,21 @@ class FpCertificate(models.Model):
"""Open email composer with the certificate PDF attached."""
self.ensure_one()
template = self.env.ref('mail.email_compose_message_wizard_form', raise_if_not_found=False)
# CoC goes to ALL the customer's CoC contacts (the cert's primary
# contact + the rest of partner.x_fc_default_coc_contact_ids),
# falling back to the company. Every contact who needs the cert is
# pre-filled on the composer.
recipients = self.contact_partner_id
if (self.partner_id
and 'x_fc_default_coc_contact_ids' in self.partner_id._fields):
recipients |= self.partner_id.x_fc_default_coc_contact_ids
partner_ids = recipients.ids or (
[self.partner_id.id] if self.partner_id else [])
ctx = {
'default_model': 'fp.certificate',
'default_res_ids': self.ids,
'default_composition_mode': 'comment',
'default_partner_ids': [self.partner_id.id] if self.partner_id else [],
'default_partner_ids': partner_ids,
}
if self.attachment_id:
ctx['default_attachment_ids'] = [self.attachment_id.id]