[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:
Adrien Widart
2022-02-11 11:40:19 +00:00
parent 4308de6aab
commit 77ca3a830d
2 changed files with 85 additions and 42 deletions
+37 -42
View File
@@ -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'):
+48
View File
@@ -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')