diff --git a/fusion_plating/fusion_plating_certificates/__manifest__.py b/fusion_plating/fusion_plating_certificates/__manifest__.py
index 356f2810..fd7a00b2 100644
--- a/fusion_plating/fusion_plating_certificates/__manifest__.py
+++ b/fusion_plating/fusion_plating_certificates/__manifest__.py
@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Certificates',
- 'version': '19.0.9.4.0',
+ 'version': '19.0.10.2.0',
'category': 'Manufacturing/Plating',
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
'description': """
diff --git a/fusion_plating/fusion_plating_certificates/migrations/19.0.10.2.0/post-migrate.py b/fusion_plating/fusion_plating_certificates/migrations/19.0.10.2.0/post-migrate.py
new file mode 100644
index 00000000..3e007852
--- /dev/null
+++ b/fusion_plating/fusion_plating_certificates/migrations/19.0.10.2.0/post-migrate.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Nexa Systems Inc.
+# License OPL-1
+"""Migrate the single Default CoC Contact (Many2one) to the multi-contact
+Many2many.
+
+The field x_fc_default_coc_contact_id (Many2one column on res_partner) was
+renamed to x_fc_default_coc_contact_ids (self-referential Many2many, rel
+table fp_default_coc_contact_rel). Odoo creates the new rel table during the
+schema-update phase but leaves the old column orphaned. Copy each partner's
+single contact into the new M2m so existing per-customer CoC routing carries
+over, then drop the dead column. Idempotent + guarded on column existence.
+"""
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+def migrate(cr, version):
+ # Old column still present (Odoo doesn't drop removed-field columns)?
+ cr.execute("""
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'res_partner'
+ AND column_name = 'x_fc_default_coc_contact_id'
+ """)
+ if not cr.fetchone():
+ return
+ # New M2m rel table created by the schema update before this runs.
+ cr.execute("""
+ SELECT 1 FROM information_schema.tables
+ WHERE table_name = 'fp_default_coc_contact_rel'
+ """)
+ if not cr.fetchone():
+ _logger.warning(
+ 'fp_default_coc_contact_rel missing — skipping CoC contact '
+ 'migration (rel table not created yet).')
+ return
+ # Copy the single value into the M2m (skip rows already present so a
+ # re-run is harmless).
+ cr.execute("""
+ INSERT INTO fp_default_coc_contact_rel (partner_id, contact_id)
+ SELECT p.id, p.x_fc_default_coc_contact_id
+ FROM res_partner p
+ WHERE p.x_fc_default_coc_contact_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1 FROM fp_default_coc_contact_rel r
+ WHERE r.partner_id = p.id
+ AND r.contact_id = p.x_fc_default_coc_contact_id)
+ """)
+ moved = cr.rowcount
+ cr.execute(
+ "ALTER TABLE res_partner DROP COLUMN IF EXISTS "
+ "x_fc_default_coc_contact_id")
+ _logger.info(
+ 'CoC contact migration: copied %s single Default-CoC-Contact value(s) '
+ 'into the new x_fc_default_coc_contact_ids M2m, dropped old column.',
+ moved)
diff --git a/fusion_plating/fusion_plating_certificates/models/fp_certificate.py b/fusion_plating/fusion_plating_certificates/models/fp_certificate.py
index e177417b..9370ac5a 100644
--- a/fusion_plating/fusion_plating_certificates/models/fp_certificate.py
+++ b/fusion_plating/fusion_plating_certificates/models/fp_certificate.py
@@ -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]
diff --git a/fusion_plating/fusion_plating_certificates/models/res_partner.py b/fusion_plating/fusion_plating_certificates/models/res_partner.py
index 5bdbd0a4..9e203999 100644
--- a/fusion_plating/fusion_plating_certificates/models/res_partner.py
+++ b/fusion_plating/fusion_plating_certificates/models/res_partner.py
@@ -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.',
)
diff --git a/fusion_plating/fusion_plating_certificates/views/res_partner_views.xml b/fusion_plating/fusion_plating_certificates/views/res_partner_views.xml
index 2c85fe74..d4de2d39 100644
--- a/fusion_plating/fusion_plating_certificates/views/res_partner_views.xml
+++ b/fusion_plating/fusion_plating_certificates/views/res_partner_views.xml
@@ -44,15 +44,17 @@
- The named contact this customer's CoC is addressed - to and emailed to. Pre-fills cert records when a - job ships. Leave blank to force the manager to pick - at issue time. + The contacts this customer's CoC is addressed to and + emailed to. The first is the primary (printed on the + cert); the rest are copied (CC) when the CoC is sent. + Pre-fills cert records when a job ships. Leave blank + to force the manager to pick at issue time.