diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index d477d393837..719c8521864 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -1949,36 +1949,28 @@ class MrpProduction(models.Model): for move_line in move.move_line_ids: if float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding): continue - domain = [ - ('lot_id', '=', move_line.lot_id.id), - ('qty_done', '=', 1), - ('state', '=', 'done') - ] message = _('The serial number %(number)s used for component %(component)s has already been consumed', number=move_line.lot_id.name, component=move_line.product_id.name) co_prod_move_lines = self.move_raw_ids.move_line_ids - domain_unbuild = domain + [ - ('production_id', '=', False), - ('location_id.usage', '=', 'production') - ] # Check presence of same sn in previous productions - duplicates = self.env['stock.move.line'].search_count(domain + [ - ('location_dest_id.usage', '=', 'production') + duplicates = self.env['stock.move.line'].search_count([ + ('lot_id', '=', move_line.lot_id.id), + ('qty_done', '=', 1), + ('state', '=', 'done'), + ('location_dest_id.usage', '=', 'production'), ]) if duplicates: # Maybe some move lines have been compensated by unbuild - duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [ - ('move_id.unbuild_id', '!=', False) - ]) + duplicates_returned = move.product_id._count_returned_sn_products(move_line.lot_id) removed = self.env['stock.move.line'].search_count([ ('lot_id', '=', move_line.lot_id.id), ('state', '=', 'done'), ('location_dest_id.scrap_location', '=', True) ]) # Either removed or unbuild - if not ((duplicates_unbuild or removed) and duplicates - duplicates_unbuild - removed == 0): + if not ((duplicates_returned or removed) and duplicates - duplicates_returned - removed == 0): raise UserError(message) # Check presence of same sn in current production duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index eb1ea0ad4e6..e4751689c07 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -261,3 +261,14 @@ class ProductProduct(models.Model): if not any(ptav in self.product_template_attribute_value_ids for ptav in iter_ptav): return False return True + + def _count_returned_sn_products(self, sn_lot): + res = self.env['stock.move.line'].search_count([ + ('lot_id', '=', sn_lot.id), + ('qty_done', '=', 1), + ('state', '=', 'done'), + ('production_id', '=', False), + ('location_id.usage', '=', 'production'), + ('move_id.unbuild_id', '!=', False), + ]) + return super()._count_returned_sn_products(sn_lot) + res diff --git a/addons/mrp_repair/tests/test_tracability.py b/addons/mrp_repair/tests/test_tracability.py index 112e4f44e44..abf96ef96b0 100644 --- a/addons/mrp_repair/tests/test_tracability.py +++ b/addons/mrp_repair/tests/test_tracability.py @@ -72,3 +72,61 @@ class TestRepairTraceability(TestMrpCommon): }) # We are not forbidden to use that serial number, so nothing raised here mo2.button_mark_done() + + def test_mo_with_used_sn_component(self): + """ + Suppose a tracked-by-usn component has been used to produce a product. Then, using a repair order, + this component is removed from the product and returned as available stock. The user should be able to + use the component in a new MO + """ + def produce_one(product, component): + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = product + with mo_form.move_raw_ids.new() as raw_line: + raw_line.product_id = component + raw_line.product_uom_qty = 1 + mo = mo_form.save() + mo.action_confirm() + mo.action_assign() + action = mo.button_mark_done() + wizard = Form(self.env[action['res_model']].with_context(action['context'])).save() + wizard.process() + return mo + + stock_location = self.env.ref('stock.stock_location_stock') + + finished, component = self.env['product.product'].create([{ + 'name': 'Finished Product', + 'type': 'product', + }, { + 'name': 'SN Componentt', + 'type': 'product', + 'tracking': 'serial', + }]) + + sn_lot = self.env['stock.lot'].create({ + 'product_id': component.id, + 'name': 'USN01', + 'company_id': self.env.company.id, + }) + self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=sn_lot) + + mo = produce_one(finished, component) + self.assertEqual(mo.state, 'done') + self.assertEqual(mo.move_raw_ids.lot_ids, sn_lot) + + ro_form = Form(self.env['repair.order']) + ro_form.product_id = finished + with ro_form.operations.new() as ro_line: + ro_line.type = 'remove' + ro_line.product_id = component + ro_line.lot_id = sn_lot + ro_line.location_dest_id = stock_location + ro = ro_form.save() + ro.action_validate() + ro.action_repair_start() + ro.action_repair_end() + + mo = produce_one(finished, component) + self.assertEqual(mo.state, 'done') + self.assertEqual(mo.move_raw_ids.lot_ids, sn_lot) diff --git a/addons/repair/models/__init__.py b/addons/repair/models/__init__.py index b082b6a40bb..4bebbca6293 100644 --- a/addons/repair/models/__init__.py +++ b/addons/repair/models/__init__.py @@ -5,3 +5,4 @@ from . import repair from . import stock_traceability from . import stock_lot from . import account_move +from . import product diff --git a/addons/repair/models/product.py b/addons/repair/models/product.py new file mode 100644 index 00000000000..3dcdf492202 --- /dev/null +++ b/addons/repair/models/product.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Product(models.Model): + _inherit = "product.product" + + def _count_returned_sn_products(self, sn_lot): + res = self.env['repair.line'].search_count([ + ('type', '=', 'remove'), + ('product_uom_qty', '=', 1), + ('lot_id', '=', sn_lot.id), + ('state', '=', 'done'), + ('location_dest_id.usage', '=', 'internal'), + ]) + return super()._count_returned_sn_products(sn_lot) + res diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 64c4305ab6b..ff5668e38c8 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -596,6 +596,10 @@ class Product(models.Model): linked_product_ids = [group['product_id'][0] for group in lines] return super(Product, self - self.browse(linked_product_ids))._filter_to_unlink() + @api.model + def _count_returned_sn_products(self, sn_lot): + return 0 + class ProductTemplate(models.Model): _inherit = 'product.template'