[FIX] mrp: produce SN product with lot of unbuilt product
To reproduce the issue: 1. Produce a SN product P with lot L 2. Unbuild it 3. Produce 1xP with lot L Error: When setting the lot, an error is displayed. When marking the MO as done, a second error is displayed Both domains in `_onchange_lot_producing` and in the beginning of `_check_sn_uniqueness` are incorrect. We should centralize the uniqueness checking thanks to all existing computations in `_check_sn_uniqueness`. This way, unbuilt/scrapped moves will be considered OPW-2721205 closes odoo/odoo#84374 X-original-commit: 2540104e95cea7baaa22ccfce97759771dbbaf34 Signed-off-by: Tiffany Chang <tic@odoo.com> Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
@@ -2008,54 +2008,16 @@ class MrpProduction(models.Model):
|
||||
def _check_sn_uniqueness(self):
|
||||
""" Alert the user if the serial number as already been consumed/produced """
|
||||
if self.product_tracking == 'serial' and self.lot_producing_id:
|
||||
sml = self.env['stock.move.line'].search_count([
|
||||
('lot_id', '=', self.lot_producing_id.id),
|
||||
('location_id.usage', '=', 'production'),
|
||||
('qty_done', '=', 1),
|
||||
('state', '=', 'done')
|
||||
])
|
||||
if sml:
|
||||
if self._is_finished_sn_already_produced(self.lot_producing_id):
|
||||
raise UserError(_('This serial number for product %s has already been produced', self.product_id.name))
|
||||
|
||||
for move in self.move_finished_ids:
|
||||
if move.has_tracking != 'serial' or move.product_id == self.product_id:
|
||||
continue
|
||||
for move_line in move.move_line_ids:
|
||||
domain = [
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('qty_done', '=', 1),
|
||||
('state', '=', 'done')
|
||||
]
|
||||
message = _('The serial number %(number)s used for byproduct %(product_name)s has already been produced',
|
||||
number=move_line.lot_id.name,
|
||||
product_name=move_line.product_id.name)
|
||||
co_prod_move_lines = self.move_finished_ids.move_line_ids.filtered(lambda ml: ml.product_id != self.product_id)
|
||||
domain_unbuild = domain + [
|
||||
('production_id', '=', False),
|
||||
('location_dest_id.usage', '=', 'production')
|
||||
]
|
||||
|
||||
# Check presence of same sn in previous productions
|
||||
duplicates = self.env['stock.move.line'].search_count(domain + [
|
||||
('location_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)
|
||||
])
|
||||
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):
|
||||
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
|
||||
if duplicates:
|
||||
raise UserError(message)
|
||||
if self._is_finished_sn_already_produced(move_line.lot_id, excluded_sml=move_line):
|
||||
raise UserError(_('The serial number %(number)s used for byproduct %(product_name)s has already been produced',
|
||||
number=move_line.lot_id.name, product_name=move_line.product_id.name))
|
||||
|
||||
for move in self.move_raw_ids:
|
||||
if move.has_tracking != 'serial':
|
||||
@@ -2091,6 +2053,39 @@ class MrpProduction(models.Model):
|
||||
if duplicates:
|
||||
raise UserError(message)
|
||||
|
||||
def _is_finished_sn_already_produced(self, lot, excluded_sml=None):
|
||||
excluded_sml = excluded_sml or self.env['stock.move.line']
|
||||
domain = [
|
||||
('lot_id', '=', lot.id),
|
||||
('qty_done', '=', 1),
|
||||
('state', '=', 'done')
|
||||
]
|
||||
co_prod_move_lines = self.move_finished_ids.move_line_ids - excluded_sml
|
||||
domain_unbuild = domain + [
|
||||
('production_id', '=', False),
|
||||
('location_dest_id.usage', '=', 'production')
|
||||
]
|
||||
# Check presence of same sn in previous productions
|
||||
duplicates = self.env['stock.move.line'].search_count(domain + [
|
||||
('location_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)
|
||||
])
|
||||
removed = self.env['stock.move.line'].search_count([
|
||||
('lot_id', '=', lot.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):
|
||||
return True
|
||||
# Check presence of same sn in current production
|
||||
duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == lot)
|
||||
return bool(duplicates)
|
||||
|
||||
def _check_immediate(self):
|
||||
immediate_productions = self.browse()
|
||||
if self.env.context.get('skip_immediate'):
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestTraceability(TestMrpCommon):
|
||||
@@ -319,3 +322,48 @@ class TestTraceability(TestMrpCommon):
|
||||
self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
byproduct_move_line_2_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_2')
|
||||
self.assertEqual(byproduct_move_line_2_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
|
||||
|
||||
def test_reuse_unbuilt_usn(self):
|
||||
"""
|
||||
Produce a SN product
|
||||
Unbuilt it
|
||||
Produce a new SN product with same lot
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='serial')
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.env['stock.quant']._update_available_quantity(p1, stock_location, 1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, stock_location, 1)
|
||||
mo.action_assign()
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo_form.lot_producing_id = lot
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
unbuild_form.lot_id = lot
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = bom
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
with self.assertLogs(level="WARNING") as log_catcher:
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo_form.lot_producing_id = lot
|
||||
mo = mo_form.save()
|
||||
_logger.warning('Dummy')
|
||||
self.assertEqual(len(log_catcher.output), 1, "Useless warnings: \n%s" % "\n".join(log_catcher.output[:-1]))
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
||||
Reference in New Issue
Block a user