Skip to content
Permalink
Browse files

[IMP] mrp: block usage of duplicate serial number

With this commit, the manufacturer will now be noticed earlier in
the process if he produced/consumed a serial number already produced/consumed in a
previous production. Before this commit, an error was triggered as
well but only at the very last step of the production.
This leaded to two issues :
 - The user had to unlock-edit-lock to update the wrong serial number
 - On large production, it was difficult to figure out which was/were
   the product(s) to fix.

The search on previous production is done each time the produce wizard is closed
or once the production is done on a workorder

Task 2002133
  • Loading branch information...
Whenrow committed Oct 7, 2019
1 parent eaa0223 commit 099f7d41926d031c40311ea57bd1e7450e997b05
@@ -299,6 +299,18 @@ def _strict_consumption_check(self):
if float_compare(qty_done, qty_to_consume, precision_rounding=rounding) != 0:
raise UserError(_('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.') % lines[0].product_id.name)

def _check_sn_uniquiness(self):
""" Alert the user if the serial number as already been produced """
if self.product_tracking == 'serial' and self.finished_lot_id:
sml = self.env['stock.move.line'].search_count([
('lot_id', '=', self.finished_lot_id.id),
('location_id.usage', '=', 'production'),
('qty_done', '=', 1),
('state', '=', 'done')
])
if sml:
raise UserError(_('This serial number for product %s has already been produced' % self.product_id.name))


class MrpAbstractWorkorderLine(models.AbstractModel):
_name = "mrp.abstract.workorder.line"
@@ -362,7 +374,10 @@ def _update_move_lines(self):
raise UserError(_('Please enter a lot or serial number for %s !' % self.product_id.display_name))

if self.lot_id and self.product_id.tracking == 'serial' and self.lot_id in self.move_id.move_line_ids.filtered(lambda ml: ml.qty_done).mapped('lot_id'):
raise UserError(_('You cannot consume the same serial number twice. Please correct the serial numbers encoded.'))
if self[self._get_raw_workorder_inverse_name()]:
raise UserError(_('The serial number used for component %s has already been consumed' % self.product_id.name))
else:
raise UserError(_('The serial number used for byproduct %s has already been produced' % self.product_id.name))

# Update reservation and quantity done
for ml in move_lines:
@@ -446,6 +461,26 @@ def _create_extra_move_lines(self):

return vals_list

def _check_line_sn_uniquiness(self):
""" Alert the user if the line serial number as already been consumed/produced """
self.ensure_one()
if self.product_tracking == 'serial' and self.lot_id:
domain = [
('lot_id', '=', self.lot_id.id),
('qty_done', '=', 1),
('state', '=', 'done')
]
# Adapt domain and error message in case of component or byproduct
if self[self._get_raw_workorder_inverse_name()]:
domain.append(('location_dest_id.usage', '=', 'production'))
message = _('The serial number used for component %s has already been consumed' % self.product_id.name)
else:
domain.append(('location_id.usage', '=', 'production'))
message = _('The serial number used for byproduct %s has already been produced' % self.product_id.name)
sml = self.env['stock.move.line'].search_count(domain)
if sml:
raise UserError(message)

def _unreserve_order(self):
""" Unreserve line with lower reserved quantity first """
self.ensure_one()
@@ -384,10 +384,13 @@ def record_production(self):
return True

self.ensure_one()
self._check_sn_uniquiness()
self._check_company()
if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))

if 'check_ids' not in self:
for line in self.raw_workorder_line_ids | self.finished_workorder_line_ids:
line._check_line_sn_uniquiness()
# If last work order, then post lots used
if not self.next_work_order_id:
self._update_finished_move()
@@ -312,7 +312,7 @@ def test_product_produce_1(self):
self.assertEqual(line1['qty_done'], 3, "Wrong quantity done")
self.assertEqual(line2['qty_to_consume'], 12, "Wrong quantity to consume")
self.assertEqual(line2['qty_done'], 12, "Wrong quantity done")

product_produce = produce_form.save()
self.assertEqual(len(product_produce.raw_workorder_line_ids), 2, 'You should have produce lines even the consumed products are not tracked.')
product_produce.do_produce()
@@ -938,6 +938,130 @@ def test_product_produce_10(self):
self.assertEqual(sum(move_lines_byproduct_3.mapped('qty_done')), 5.0)
self.assertEqual(move_lines_byproduct_3.mapped('product_uom_id'), dozen)

def test_product_produce_duplicate_1(self):
""" produce a finished product tracked by serial number 2 times with the
same SN. Check that an error is raised the second time"""
mo1, bom, p_final, p1, p2 = self.generate_mo(tracking_final='serial', qty_final=1, qty_base_1=1,)

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo1.id,
'active_ids': [mo1.id],
}))
product_produce = produce_form.save()
product_produce.action_generate_serial()
sn = product_produce.finished_lot_id
product_produce.do_produce()
mo1.button_mark_done()

mo_form = Form(self.env['mrp.production'])
mo_form.product_id = p_final
mo_form.bom_id = bom
mo_form.product_qty = 1
mo2 = mo_form.save()
mo2.action_confirm()

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo2.id,
'active_ids': [mo2.id],
}))
produce_form.finished_lot_id = sn
product_produce = produce_form.save()
with self.assertRaises(UserError):
product_produce.do_produce()

def test_product_produce_duplicate_2(self):
""" produce a finished product with component tracked by serial number 2
times with the same SN. Check that an error is raised the second time"""
mo1, bom, p_final, p1, p2 = self.generate_mo(tracking_base_2='serial', qty_final=1, qty_base_1=1,)
sn = self.env['stock.production.lot'].create({
'name': 'sn used twice',
'product_id': p2.id,
'company_id': self.env.company.id,
})
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo1.id,
'active_ids': [mo1.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
product_produce.do_produce()
mo1.button_mark_done()

mo_form = Form(self.env['mrp.production'])
mo_form.product_id = p_final
mo_form.bom_id = bom
mo_form.product_qty = 1
mo2 = mo_form.save()
mo2.action_confirm()

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo2.id,
'active_ids': [mo2.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
with self.assertRaises(UserError):
product_produce.do_produce()

def test_product_produce_duplicate_3(self):
""" produce a finished product with by-product tracked by serial number 2
times with the same SN. Check that an error is raised the second time"""
finished_product = self.env['product.product'].create({'name': 'finished product'})
byproduct = self.env['product.product'].create({'name': 'byproduct', 'tracking': 'serial'})
component = self.env['product.product'].create({'name': 'component'})
bom = self.env['mrp.bom'].create({
'product_id': finished_product.id,
'product_tmpl_id': finished_product.product_tmpl_id.id,
'product_uom_id': finished_product.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': component.id, 'product_qty': 1}),
],
'byproduct_ids': [
(0, 0, {'product_id': byproduct.id, 'product_qty': 1, 'product_uom_id': byproduct.uom_id.id})
]})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.bom_id = bom
mo_form.product_qty = 1
mo = mo_form.save()
mo.action_confirm()

sn = self.env['stock.production.lot'].create({
'name': 'sn used twice',
'product_id': byproduct.id,
'company_id': self.env.company.id,
})
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
with produce_form.finished_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
product_produce.do_produce()
mo.button_mark_done()

mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.bom_id = bom
mo_form.product_qty = 1
mo2 = mo_form.save()
mo2.action_confirm()

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo2.id,
'active_ids': [mo2.id],
}))
with produce_form.finished_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
with self.assertRaises(UserError):
product_produce.do_produce()

def test_product_produce_uom(self):
""" Produce a finished product tracked by serial number. Set another
UoM on the bom. The produce wizard should keep the UoM of the product (unit)
@@ -137,6 +137,7 @@ def _record_production(self):
move_id = self.env['stock.move'].create(values)
line.move_id = move_id.id

line._check_line_sn_uniquiness()
# because of an ORM limitation (fields on transient models are not
# recomputed by updates in non-transient models), the related fields on
# this model are not recomputed by the creations above
@@ -146,6 +147,8 @@ def _record_production(self):
quantity = self.qty_producing
if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
raise UserError(_("The production order for '%s' has no quantity specified.") % self.product_id.display_name)

self._check_sn_uniquiness()
self._update_finished_move()
self._update_moves()
if self.production_id.state == 'confirmed':

0 comments on commit 099f7d4

Please sign in to comment.
You can’t perform that action at this time.