Settings flag controls which SO confirm path runs. Default False keeps the legacy bridge_mrp / mrp.production flow on entech. Setting True diverts confirm into fp.job creation. Both hooks coexist — bridge_mrp's _fp_auto_create_mo and the new _fp_auto_create_job — but only one creates records per SO confirm (controlled by the flag). The new _fp_auto_create_job mirrors bridge_mrp's grouping logic (x_fc_wo_group_tag), recipe resolution (coating → part), and traceability fields (origin, sale_order_line_ids). Settings UI shows the flag in a 'Fusion Plating Jobs' app section of the standard Configuration menu. 3 new tests cover: flag off no-op, flag on creates job, idempotency. Manifest 19.0.1.3.0 → 19.0.1.4.0. Part of: native job model migration (spec 2026-04-25) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
#
|
|
# sale.order.action_confirm hook — creates fp.job records when the
|
|
# x_fc_use_native_jobs setting is True. Mirrors bridge_mrp's
|
|
# _fp_auto_create_mo but creates fp.job instead of mrp.production.
|
|
#
|
|
# When the setting is False (default), this hook is a no-op and
|
|
# bridge_mrp's MO-creation hook handles the flow.
|
|
|
|
import logging
|
|
from odoo import _, api, fields, models
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SaleOrder(models.Model):
|
|
_inherit = 'sale.order'
|
|
|
|
def action_confirm(self):
|
|
result = super().action_confirm()
|
|
# Only run when the native flag is on
|
|
ICP = self.env['ir.config_parameter'].sudo()
|
|
if ICP.get_param('fusion_plating_jobs.use_native_jobs') == 'True':
|
|
for so in self:
|
|
so._fp_auto_create_job()
|
|
return result
|
|
|
|
def _fp_auto_create_job(self):
|
|
"""Create fp.job(s) from the SO's plating lines.
|
|
|
|
Lines that share a `x_fc_wo_group_tag` collapse into one job;
|
|
untagged lines get one job per line. Mirrors bridge_mrp's
|
|
_fp_auto_create_mo grouping logic.
|
|
"""
|
|
self.ensure_one()
|
|
Job = self.env['fp.job'].sudo()
|
|
|
|
# Idempotency: skip if a job already references this SO
|
|
existing = Job.search([('sale_order_id', '=', self.id)], limit=1)
|
|
if existing:
|
|
return
|
|
|
|
# Find plating lines (those with a part_catalog_id or coating_config_id)
|
|
plating_lines = self.order_line.filtered(
|
|
lambda l: (
|
|
('x_fc_part_catalog_id' in l._fields and l.x_fc_part_catalog_id)
|
|
or ('x_fc_coating_config_id' in l._fields and l.x_fc_coating_config_id)
|
|
)
|
|
)
|
|
if not plating_lines:
|
|
_logger.info('SO %s: no plating lines, skipping job creation.', self.name)
|
|
return
|
|
|
|
# Group by x_fc_wo_group_tag (untagged → distinct group per line)
|
|
groups = {} # tag → recordset of lines
|
|
untagged_idx = 0
|
|
for line in plating_lines:
|
|
tag = (
|
|
'x_fc_wo_group_tag' in line._fields and line.x_fc_wo_group_tag
|
|
) or False
|
|
if not tag:
|
|
untagged_idx += 1
|
|
tag = '__untagged_%d' % untagged_idx
|
|
groups[tag] = groups.get(tag, self.env['sale.order.line']) | line
|
|
|
|
# Create a job per group
|
|
for tag, lines in groups.items():
|
|
first_line = lines[0]
|
|
qty = sum(lines.mapped('product_uom_qty'))
|
|
part = (
|
|
'x_fc_part_catalog_id' in first_line._fields
|
|
and first_line.x_fc_part_catalog_id
|
|
or False
|
|
)
|
|
coating = (
|
|
'x_fc_coating_config_id' in first_line._fields
|
|
and first_line.x_fc_coating_config_id
|
|
or False
|
|
)
|
|
# Recipe lookup: from coating, fallback to part
|
|
recipe = False
|
|
if coating and 'recipe_id' in coating._fields and coating.recipe_id:
|
|
recipe = coating.recipe_id
|
|
if not recipe and part and 'default_process_id' in part._fields and part.default_process_id:
|
|
recipe = part.default_process_id
|
|
if not recipe and part and 'recipe_id' in part._fields and part.recipe_id:
|
|
recipe = part.recipe_id
|
|
|
|
vals = {
|
|
'partner_id': self.partner_id.id,
|
|
'product_id': first_line.product_id.id if first_line.product_id else False,
|
|
'qty': qty,
|
|
'origin': self.name,
|
|
'sale_order_id': self.id,
|
|
'sale_order_line_ids': [(6, 0, lines.ids)],
|
|
'date_deadline': self.commitment_date or self.date_order,
|
|
}
|
|
if part:
|
|
vals['part_catalog_id'] = part.id
|
|
if coating:
|
|
vals['coating_config_id'] = coating.id
|
|
if recipe:
|
|
vals['recipe_id'] = recipe.id
|
|
|
|
# Customer spec / facility / manager — copy from SO if present
|
|
if 'x_fc_customer_spec_id' in self._fields and self.x_fc_customer_spec_id:
|
|
vals['customer_spec_id'] = self.x_fc_customer_spec_id.id
|
|
if 'x_fc_facility_id' in self._fields and self.x_fc_facility_id:
|
|
vals['facility_id'] = self.x_fc_facility_id.id
|
|
if 'x_fc_manager_id' in self._fields and self.x_fc_manager_id:
|
|
vals['manager_id'] = self.x_fc_manager_id.id
|
|
|
|
# Quoted revenue: sum line totals
|
|
vals['quoted_revenue'] = sum(lines.mapped('price_subtotal'))
|
|
|
|
job = Job.create(vals)
|
|
_logger.info(
|
|
'SO %s: created fp.job %s (qty=%s, recipe=%s)',
|
|
self.name, job.name, qty, (recipe.name if recipe else '-'),
|
|
)
|
|
return True
|