feat(fusion_plating_jobs): multi-part cert creation + requirement union
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -609,38 +609,47 @@ class FpJob(models.Model):
|
||||
matches the defensive pattern used elsewhere in this file.
|
||||
"""
|
||||
self.ensure_one()
|
||||
# ---- Step 1 — partner + part baseline ----
|
||||
req = (
|
||||
self.part_catalog_id
|
||||
and self.part_catalog_id.certificate_requirement
|
||||
) or 'inherit'
|
||||
if req == 'inherit':
|
||||
wanted = set()
|
||||
# ---- Step 1 — partner + part baseline (union across all parts) ----
|
||||
def _partner_inherit_set():
|
||||
s = set()
|
||||
p = self.partner_id
|
||||
if p:
|
||||
if p.x_fc_send_coc:
|
||||
wanted.add('coc')
|
||||
s.add('coc')
|
||||
if p.x_fc_send_thickness_report:
|
||||
wanted.add('thickness_report')
|
||||
# Three aerospace/defence partner toggles. Field guards
|
||||
# let this module load even if fusion_plating_certificates
|
||||
# is at an older version that pre-dates the new fields.
|
||||
if ('x_fc_send_nadcap_cert' in p._fields
|
||||
and p.x_fc_send_nadcap_cert):
|
||||
wanted.add('nadcap_cert')
|
||||
if ('x_fc_send_mill_test' in p._fields
|
||||
and p.x_fc_send_mill_test):
|
||||
wanted.add('mill_test')
|
||||
if ('x_fc_send_customer_specific' in p._fields
|
||||
and p.x_fc_send_customer_specific):
|
||||
wanted.add('customer_specific')
|
||||
else:
|
||||
wanted = {
|
||||
'none': set(),
|
||||
'coc': {'coc'},
|
||||
s.add('thickness_report')
|
||||
if 'x_fc_send_nadcap_cert' in p._fields and p.x_fc_send_nadcap_cert:
|
||||
s.add('nadcap_cert')
|
||||
if 'x_fc_send_mill_test' in p._fields and p.x_fc_send_mill_test:
|
||||
s.add('mill_test')
|
||||
if 'x_fc_send_customer_specific' in p._fields and p.x_fc_send_customer_specific:
|
||||
s.add('customer_specific')
|
||||
return s
|
||||
|
||||
def _explicit_set(req):
|
||||
return {
|
||||
'none': set(), 'coc': {'coc'},
|
||||
'coc_thickness': {'coc', 'thickness_report'},
|
||||
}.get(req, {'coc'})
|
||||
|
||||
parts = self._fp_cert_source_lines().mapped('x_fc_part_catalog_id')
|
||||
if not parts and self.part_catalog_id:
|
||||
parts = self.part_catalog_id
|
||||
if not parts:
|
||||
parts = [False]
|
||||
wanted = set()
|
||||
inherit = None
|
||||
for part in parts:
|
||||
req = (part.certificate_requirement
|
||||
if part and 'certificate_requirement' in part._fields
|
||||
else 'inherit') or 'inherit'
|
||||
if req == 'inherit':
|
||||
if inherit is None:
|
||||
inherit = _partner_inherit_set()
|
||||
wanted |= inherit
|
||||
else:
|
||||
wanted |= _explicit_set(req)
|
||||
|
||||
# ---- Step 2 — recipe suppression (suppress-only) ----
|
||||
recipe = self.recipe_id
|
||||
if recipe:
|
||||
@@ -2655,6 +2664,58 @@ class FpJob(models.Model):
|
||||
self.name, e,
|
||||
)
|
||||
|
||||
def _fp_cert_source_lines(self):
|
||||
"""Plating SO lines this job covers (one cert part-line each)."""
|
||||
self.ensure_one()
|
||||
lines = self.sale_order_line_ids
|
||||
if not lines and self.sale_order_id:
|
||||
lines = self.sale_order_id.order_line
|
||||
return lines.filtered(
|
||||
lambda l: not l.display_type
|
||||
and ('x_fc_part_catalog_id' in l._fields and l.x_fc_part_catalog_id))
|
||||
|
||||
def _fp_format_spec_ref(self, spec):
|
||||
"""Format 'CODE Rev X' from a customer spec (or '')."""
|
||||
if not spec:
|
||||
return ''
|
||||
ref = spec.code or ''
|
||||
if 'revision' in spec._fields and spec.revision:
|
||||
ref = (f'{ref} Rev {spec.revision}' if ref
|
||||
else f'Rev {spec.revision}')
|
||||
return ref
|
||||
|
||||
def _fp_build_cert_part_commands(self):
|
||||
"""O2M create commands for fp.certificate.part — one per line."""
|
||||
self.ensure_one()
|
||||
cmds, seq = [], 10
|
||||
for sol in self._fp_cert_source_lines():
|
||||
part = sol.x_fc_part_catalog_id
|
||||
spec = (sol.x_fc_customer_spec_id
|
||||
if 'x_fc_customer_spec_id' in sol._fields else False)
|
||||
serials = ''
|
||||
if 'x_fc_serial_ids' in sol._fields and sol.x_fc_serial_ids:
|
||||
serials = ', '.join(sol.x_fc_serial_ids.mapped('name'))
|
||||
# fp_customer_description() is a method (configurator), not a
|
||||
# field — use hasattr, not a _fields check.
|
||||
desc = (sol.fp_customer_description()
|
||||
if hasattr(sol, 'fp_customer_description')
|
||||
else (sol.name or ''))
|
||||
cmds.append((0, 0, {
|
||||
'sequence': seq,
|
||||
'sale_order_line_id': sol.id,
|
||||
'part_catalog_id': part.id if part else False,
|
||||
'part_number': (part.part_number if part else '') or '',
|
||||
'part_name': (part.name if part else '') or '',
|
||||
'description': desc,
|
||||
'serial': serials,
|
||||
'customer_spec_id': spec.id if spec else False,
|
||||
'spec_reference': self._fp_format_spec_ref(spec),
|
||||
'quantity_shipped': int(sol.product_uom_qty or 0),
|
||||
'nc_quantity': 0,
|
||||
}))
|
||||
seq += 10
|
||||
return cmds
|
||||
|
||||
def _fp_create_certificates(self):
|
||||
"""Auto-create one draft fp.certificate per type returned by
|
||||
_resolve_required_cert_types. Idempotent per type — re-running
|
||||
@@ -2742,10 +2803,7 @@ class FpJob(models.Model):
|
||||
# spec_reference is what action_issue blocks on.
|
||||
# Format spec.code + revision for the cert text.
|
||||
if spec and 'spec_reference' in Cert._fields:
|
||||
ref = spec.code or ''
|
||||
if spec.revision:
|
||||
ref = (f'{ref} Rev {spec.revision}'
|
||||
if ref else f'Rev {spec.revision}')
|
||||
ref = self._fp_format_spec_ref(spec)
|
||||
if ref:
|
||||
vals['spec_reference'] = ref
|
||||
if 'customer_spec_id' in Cert._fields:
|
||||
@@ -2781,6 +2839,10 @@ class FpJob(models.Model):
|
||||
vals['contact_partner_id'] = contact.id
|
||||
if 'entech_wo_number' in Cert._fields:
|
||||
vals['entech_wo_number'] = self.name or ''
|
||||
if 'part_line_ids' in Cert._fields:
|
||||
part_cmds = self._fp_build_cert_part_commands()
|
||||
if part_cmds:
|
||||
vals['part_line_ids'] = part_cmds
|
||||
cert = Cert.create(vals)
|
||||
self.message_post(body=Markup(_(
|
||||
'%(t)s <b>%(n)s</b> auto-created (draft). Issuer '
|
||||
|
||||
@@ -10,3 +10,4 @@ from . import test_autopause_cron
|
||||
from . import test_post_shop_states
|
||||
from . import test_recipe_cert_suppression
|
||||
from . import test_order_ship_state
|
||||
from . import test_combined_cert_creation
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestCombinedCertCreation(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = self.env['res.partner'].create({
|
||||
'name': 'CertCust',
|
||||
'x_fc_send_coc': True, # drives the coc requirement
|
||||
})
|
||||
self.product = self.env['product.product'].create({'name': 'W'})
|
||||
self.part_a = self.env['fp.part.catalog'].create({
|
||||
'name': 'PartA', 'partner_id': self.partner.id, 'part_number': 'A-1'})
|
||||
self.part_b = self.env['fp.part.catalog'].create({
|
||||
'name': 'PartB', 'partner_id': self.partner.id, 'part_number': 'B-2'})
|
||||
self.so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
(0, 0, {'product_id': self.product.id, 'product_uom_qty': 3,
|
||||
'x_fc_part_catalog_id': self.part_a.id}),
|
||||
(0, 0, {'product_id': self.product.id, 'product_uom_qty': 2,
|
||||
'x_fc_part_catalog_id': self.part_b.id}),
|
||||
],
|
||||
})
|
||||
|
||||
def test_combined_cert_has_one_line_per_so_line(self):
|
||||
job = self.env['fp.job'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 5.0,
|
||||
'sale_order_id': self.so.id,
|
||||
'part_catalog_id': self.part_a.id,
|
||||
'sale_order_line_ids': [(6, 0, self.so.order_line.ids)],
|
||||
})
|
||||
job._fp_create_certificates()
|
||||
cert = self.env['fp.certificate'].search([('x_fc_job_id', '=', job.id)])
|
||||
self.assertEqual(len(cert), 1, 'one combined CoC')
|
||||
self.assertEqual(len(cert.part_line_ids), 2, 'one part-line per SO line')
|
||||
self.assertEqual(
|
||||
set(cert.part_line_ids.mapped('part_number')), {'A-1', 'B-2'})
|
||||
a = cert.part_line_ids.filtered(lambda p: p.part_number == 'A-1')
|
||||
self.assertEqual(a.quantity_shipped, 3, 'shipped qty from the line')
|
||||
|
||||
def test_part_lines_fall_back_to_so_order_line(self):
|
||||
# Job without an explicit sale_order_line_ids M2M still builds
|
||||
# one part-line per plating line via the SO order_line fallback.
|
||||
job = self.env['fp.job'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 5.0,
|
||||
'sale_order_id': self.so.id,
|
||||
'part_catalog_id': self.part_a.id,
|
||||
})
|
||||
job._fp_create_certificates()
|
||||
cert = self.env['fp.certificate'].search([('x_fc_job_id', '=', job.id)])
|
||||
self.assertEqual(len(cert), 1)
|
||||
self.assertEqual(len(cert.part_line_ids), 2,
|
||||
'falls back to SO order_line when no M2M lines set')
|
||||
Reference in New Issue
Block a user