Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion addons/mrp_subcontracting/models/mrp_production.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@ def write(self, vals):
unauthorized_fields = set(vals.keys()) - set(self._get_writeable_fields_portal_user())
if unauthorized_fields:
raise AccessError(_("You cannot write on fields %s in mrp.production.", ', '.join(unauthorized_fields)))
return super().write(vals)
res = super().write(vals)
if 'lot_producing_id' in vals and vals.get('lot_producing_id'):
for production in self:
if production._should_auto_record_subcontract_components():
try:
production.subcontracting_record_component()
except Exception:
pass
return res

def action_merge(self):
if any(production._get_subcontract_move() for production in self):
Expand Down Expand Up @@ -204,3 +212,26 @@ def _subcontract_sanity_check(self):
if sml.tracking != 'none' and not sml.lot_id:
raise UserError(_('You must enter a serial number for each line of %s') % sml.product_id.display_name)
return True

def action_generate_serial(self):
res = super().action_generate_serial()
if self._should_auto_record_subcontract_components():
try:
self.subcontracting_record_component()
except Exception:
pass
return res

def _should_auto_record_subcontract_components(self):
self.ensure_one()
if (
not self.lot_producing_id
or self.subcontracting_has_been_recorded
or not self._get_subcontract_move()
or self.state in ('done', 'cancel')
or self.product_id.tracking != 'serial'
):
return False
if not self.move_raw_ids or not any(self.move_raw_ids.mapped('quantity_done')):
return False
return True
67 changes: 67 additions & 0 deletions addons/mrp_subcontracting/tests/test_subcontracting.py
Original file line number Diff line number Diff line change
Expand Up @@ -1880,3 +1880,70 @@ def test_bom_subcontracting_product_dynamic_attribute(self):
})
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom.id, searchVariant=False)
self.assertTrue(report_values)

def test_subcontracting_serial_mass_produce(self):
"""
Test that Mass Produce on a subcontracting MO with serial-tracked finished
products properly records subcontracted components and syncs serials to receipt.
"""
# Set finished product to serial tracking
self.finished.tracking = 'serial'

# Get subcontract location
subcontract_location = self.subcontractor_partner1.property_stock_subcontractor

# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids_without_package.new() as move:
move.product_id = self.finished
move.product_uom_qty = 2
receipt = picking_form.save()
receipt.action_confirm()

# Add components to subcontractor location
self.env['stock.quant']._update_available_quantity(
self.comp1, subcontract_location, 2
)
self.env['stock.quant']._update_available_quantity(
self.comp2, subcontract_location, 2
)

# Get the created manufacturing order
production = receipt.move_ids.move_orig_ids.production_id
production.action_assign()

# Use Mass Produce wizard to assign serial numbers
action = production.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
wizard.next_serial_number = 'SN0001'
wizard.next_serial_count = 2
action = wizard.save().generate_serial_numbers_production()
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard.save().apply()

# Get all productions after split
productions = receipt.move_ids.move_orig_ids.production_id.sorted('id')

# Verify: should have 2 productions (one per serial)
self.assertEqual(len(productions), 2)

# Verify: subcontracting_has_been_recorded should be True for both
self.assertEqual(
productions.mapped('subcontracting_has_been_recorded'), [True, True],
"Mass Produce should have recorded subcontracted components for both MOs"
)

# Verify: serials are assigned to productions
self.assertEqual(
productions.mapped('lot_producing_id.name'), ['SN0001', 'SN0002'],
"Serial numbers should be assigned to split productions"
)

# Verify: serials are synced to receipt move lines
self.assertEqual(
sorted(receipt.move_line_ids.mapped('lot_id.name')),
['SN0001', 'SN0002'],
"Serial numbers should be synced to receipt move lines"
)
1 change: 1 addition & 0 deletions addons/mrp_subcontracting/wizard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from . import stock_picking_return
from . import mrp_consumption_warning
from . import change_production_qty
from . import stock_assign_serial_numbers
30 changes: 30 additions & 0 deletions addons/mrp_subcontracting/wizard/stock_assign_serial_numbers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import models


class StockAssignSerialNumbers(models.TransientModel):
_inherit = 'stock.assign.serial'

def _assign_serial_numbers(self, cancel_remaining_quantity=False):
serial_numbers = set(self._get_serial_numbers())
res = super()._assign_serial_numbers(cancel_remaining_quantity)
if not serial_numbers:
return res
# Record subcontracted components for each production with assigned serial
# When Mass Produce splits a subcontracting MO and assigns serials,
# subcontracting_record_component() is not called automatically,
# causing finished serials to not sync to receipt move lines
productions = (
self.production_id.procurement_group_id.mrp_production_ids.filtered(
lambda mo: mo.state not in ('done', 'cancel')
and not mo.subcontracting_has_been_recorded
and mo._get_subcontract_move()
and mo.lot_producing_id
and mo.lot_producing_id.name in serial_numbers
)
)
for production in productions:
production.subcontracting_record_component()
return res