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]

View File

@@ -128,17 +128,24 @@ class ResPartner(models.Model):
'who require specific NIST or DFARS language.',
)
# ---- Default CoC contact (cert addressee + email recipient) ----------
# The single named contact printed on the CoC and used as the email
# default when the cert ships. Sales sets it once per customer.
# Falls back to manual selection at action_issue time if blank.
x_fc_default_coc_contact_id = fields.Many2one(
# ---- Default CoC contacts (cert addressees + email recipients) -------
# One or more named contacts who receive this customer's CoC. The first
# is the primary (printed on the cert + pre-fills cert.contact_partner_id
# when a job ships); the rest are CC'd when the CoC is emailed. Sales
# sets the list once per customer. Falls back to manual selection at
# action_issue time if blank. Self-referential M2m (renamed from the old
# single Many2one x_fc_default_coc_contact_id — see migration 19.0.10.2.0).
x_fc_default_coc_contact_ids = fields.Many2many(
'res.partner',
string='Default CoC Contact',
relation='fp_default_coc_contact_rel',
column1='partner_id', column2='contact_id',
string='Default CoC Contacts',
domain="[('parent_id', '=', id), ('is_company', '=', False)]",
tracking=True,
help='Default contact the Certificate of Conformance is addressed '
'to and emailed to. Pre-fills cert.contact_partner_id when a '
'job ships. Leave blank to force the manager to pick at '
'issue time. Must be a child contact of this company.',
help='Contacts the Certificate of Conformance is addressed to and '
'emailed to. The first contact is the primary (printed on the '
'cert and pre-filled as the cert contact when a job ships); '
'the rest are copied (CC) when the CoC is sent. Leave blank to '
'force the manager to pick at issue time. Child contacts of '
'this company only.',
)