Files
odoo_source/addons/mrp/tests/test_traceability.py
T
Adrien Widart b622f3b9b1 [FIX] mrp: produce a tracked and manufactured component
In some cases, it is not possible to produce a tracked product that has
been used as a component of another product.

To reproduce the issue:
1. Create three products A, B, C:
    - A storable
    - B storable and tracked by lot
    - C consumable
2. Update on-hand qty of B
    - 10 x lot L01
    - 5 x lot L02
3. Create two bills of materials
    - Product: A
        - Components: 1 x B
    - Product: B
        - Components: 1 x C
4. Produce 15 x A (MO01)
    - Note: it should use 10 L01 and 5 L02
5. Produce 15 x B with lot L03 (MO02)

Error: When marking MO02 as done, a user error is displayed: "Cannot set
the done quantity from this stock move, work directly with the move
lines." The user should be able to mark the MO as done.

When confirming MO02, it confirms the associated SMs. Since the
conditions are respected, we try to assign both consumed and finished
moves. Because of step 4, there are two quants for the product B in the
production location (one for L01 and another for L02). Therefore, when
reserving the qty of the finished move, two SML are generated. As a
result, when marking MO02 as done, we write the `quantity_done` on the
finished move. This triggers the inverse method and leads to
`_multi_line_quantity_done_set`, where the error is raised because the
done quantities are not defined on each SML:
https://github.com/odoo/odoo/blob/1354081e43b71edbd434dccc6df6d0881c672a81/addons/stock/models/stock_move.py#L363-L364

Thanks to `_set_quantity_done`, we can write these done quantities on
each SML (and then the incorrect lots will be replaced by the producing
lot).

OPW-2752436

closes odoo/odoo#89015

X-original-commit: 65efb3ec5416fab6bb9ba0a015565b2abb3928cd
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
2022-04-19 15:33:53 +02:00

437 lines
19 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
import logging
_logger = logging.getLogger(__name__)
class TestTraceability(TestMrpCommon):
TRACKING_TYPES = ['none', 'serial', 'lot']
def _create_product(self, tracking):
return self.env['product.product'].create({
'name': 'Product %s' % tracking,
'type': 'product',
'tracking': tracking,
'categ_id': self.env.ref('product.product_category_all').id,
})
def test_tracking_types_on_mo(self):
finished_no_track = self._create_product('none')
finished_lot = self._create_product('lot')
finished_serial = self._create_product('serial')
consumed_no_track = self._create_product('none')
consumed_lot = self._create_product('lot')
consumed_serial = self._create_product('serial')
stock_id = self.env.ref('stock.stock_location_stock').id
Lot = self.env['stock.lot']
# create inventory
quants = self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_no_track.id,
'inventory_quantity': 3
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_lot.id,
'inventory_quantity': 3,
'lot_id': Lot.create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_serial.id,
'inventory_quantity': 1,
'lot_id': Lot.create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_serial.id,
'inventory_quantity': 1,
'lot_id': Lot.create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_serial.id,
'inventory_quantity': 1,
'lot_id': Lot.create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
})
quants.action_apply_inventory()
for finished_product in [finished_no_track, finished_lot, finished_serial]:
bom = self.env['mrp.bom'].create({
'product_id': finished_product.id,
'product_tmpl_id': finished_product.product_tmpl_id.id,
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': consumed_no_track.id, 'product_qty': 1}),
(0, 0, {'product_id': consumed_lot.id, 'product_qty': 1}),
(0, 0, {'product_id': consumed_serial.id, 'product_qty': 1}),
],
})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.bom_id = bom
mo_form.product_uom_id = self.env.ref('uom.product_uom_unit')
mo_form.product_qty = 1
mo = mo_form.save()
mo.action_confirm()
mo.action_assign()
# Start MO production
mo_form = Form(mo)
mo_form.qty_producing = 1
if finished_product.tracking != 'none':
mo_form.lot_producing_id = self.env['stock.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id})
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(mo.move_raw_ids[2], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 1
details_operation_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check results of traceability
context = ({
'active_id': mo.id,
'model': 'mrp.production',
})
lines = self.env['stock.traceability.report'].with_context(context).get_lines()
self.assertEqual(len(lines), 1, "Should always return 1 line : the final product")
final_product = lines[0]
self.assertEqual(final_product['unfoldable'], True, "Final product should always be unfoldable")
# Find parts of the final products
lines = self.env['stock.traceability.report'].get_lines(final_product['id'], **{
'level': final_product['level'],
'model_id': final_product['model_id'],
'model_name': final_product['model'],
})
self.assertEqual(len(lines), 3, "There should be 3 lines. 1 for untracked, 1 for lot, and 1 for serial")
for line in lines:
tracking = line['columns'][1].split(' ')[1]
self.assertEqual(
line['columns'][-1], "1.00 Units", 'Part with tracking type "%s", should have quantity = 1' % (tracking)
)
unfoldable = False if tracking == 'none' else True
self.assertEqual(
line['unfoldable'],
unfoldable,
'Parts with tracking type "%s", should have be unfoldable : %s' % (tracking, unfoldable)
)
def test_tracking_on_byproducts(self):
product_final = self.env['product.product'].create({
'name': 'Finished Product',
'type': 'product',
'tracking': 'serial',
})
product_1 = self.env['product.product'].create({
'name': 'Raw 1',
'type': 'product',
'tracking': 'serial',
})
product_2 = self.env['product.product'].create({
'name': 'Raw 2',
'type': 'product',
'tracking': 'serial',
})
byproduct_1 = self.env['product.product'].create({
'name': 'Byproduct 1',
'type': 'product',
'tracking': 'serial',
})
byproduct_2 = self.env['product.product'].create({
'name': 'Byproduct 2',
'type': 'product',
'tracking': 'serial',
})
bom_1 = self.env['mrp.bom'].create({
'product_id': product_final.id,
'product_tmpl_id': product_final.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'consumption': 'flexible',
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_1.id, 'product_qty': 1}),
(0, 0, {'product_id': product_2.id, 'product_qty': 1})
],
'byproduct_ids': [
(0, 0, {'product_id': byproduct_1.id, 'product_qty': 1, 'product_uom_id': byproduct_1.uom_id.id}),
(0, 0, {'product_id': byproduct_2.id, 'product_qty': 1, 'product_uom_id': byproduct_2.uom_id.id})
]})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_final
mo_form.bom_id = bom_1
mo_form.product_qty = 2
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.lot_producing_id = self.env['stock.lot'].create({
'product_id': product_final.id,
'name': 'Final_lot_1',
'company_id': self.env.company.id,
})
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.lot'].create({
'product_id': product_1.id,
'name': 'Raw_1_lot_1',
'company_id': self.env.company.id,
})
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.lot'].create({
'product_id': product_2.id,
'name': 'Raw_2_lot_1',
'company_id': self.env.company.id,
})
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.lot'].create({
'product_id': byproduct_1.id,
'name': 'Byproduct_1_lot_1',
'company_id': self.env.company.id,
})
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.lot'].create({
'product_id': byproduct_2.id,
'name': 'Byproduct_2_lot_1',
'company_id': self.env.company.id,
})
ml.qty_done = 1
details_operation_form.save()
action = mo.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
mo_form = Form(mo_backorder)
mo_form.lot_producing_id = self.env['stock.lot'].create({
'product_id': product_final.id,
'name': 'Final_lot_2',
'company_id': self.env.company.id,
})
mo_form.qty_producing = 1
mo_backorder = mo_form.save()
details_operation_form = Form(
mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_1),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.lot'].create({
'product_id': product_1.id,
'name': 'Raw_1_lot_2',
'company_id': self.env.company.id,
})
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_2),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.lot'].create({
'product_id': product_2.id,
'name': 'Raw_2_lot_2',
'company_id': self.env.company.id,
})
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.lot'].create({
'product_id': byproduct_1.id,
'name': 'Byproduct_1_lot_2',
'company_id': self.env.company.id,
})
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.lot'].create({
'product_id': byproduct_2.id,
'name': 'Byproduct_2_lot_2',
'company_id': self.env.company.id,
})
ml.qty_done = 1
details_operation_form.save()
mo_backorder.button_mark_done()
# self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4)
# self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6)
mo = mo | mo_backorder
raw_move_lines = mo.move_raw_ids.mapped('move_line_ids')
raw_line_raw_1_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_1')
self.assertEqual(set(raw_line_raw_1_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
raw_line_raw_2_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_1')
self.assertEqual(set(raw_line_raw_2_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
finished_move_lines = mo.move_finished_ids.mapped('move_line_ids')
finished_move_line_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_1')
self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
finished_move_line_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_2')
raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2')
raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2')
self.assertEqual(finished_move_line_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
byproduct_move_line_1_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_1')
self.assertEqual(byproduct_move_line_1_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_1_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_2')
self.assertEqual(byproduct_move_line_1_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
byproduct_move_line_2_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_1')
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')
def test_tracked_and_manufactured_component(self):
"""
Suppose this structure:
productA --|- 1 x productB --|- 1 x productC
with productB tracked by lot
Ensure that, when we already have some qty of productB (with different lots),
the user can produce several productA and can then produce some productB again
"""
stock_location = self.env.ref('stock.stock_location_stock')
productA, productB, productC = self.env['product.product'].create([{
'name': 'Product A',
'type': 'product',
}, {
'name': 'Product B',
'type': 'product',
'tracking': 'lot',
}, {
'name': 'Product C',
'type': 'consu',
}])
lot_B01, lot_B02, lot_B03 = self.env['stock.lot'].create([{
'name': 'lot %s' % i,
'product_id': productB.id,
'company_id': self.env.company.id,
} for i in [1, 2, 3]])
self.env['mrp.bom'].create([{
'product_id': finished.id,
'product_tmpl_id': finished.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})],
} for finished, component in [(productA, productB), (productB, productC)]])
self.env['stock.quant']._update_available_quantity(productB, stock_location, 10, lot_id=lot_B01)
self.env['stock.quant']._update_available_quantity(productB, stock_location, 5, lot_id=lot_B02)
# Produce 15 x productA
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = productA
mo_form.product_qty = 15
mo = mo_form.save()
mo.action_confirm()
action = mo.button_mark_done()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
# Produce 15 x productB
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = productB
mo_form.product_qty = 15
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 15
mo_form.lot_producing_id = lot_B03
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(lot_B01.product_qty, 0)
self.assertEqual(lot_B02.product_qty, 0)
self.assertEqual(lot_B03.product_qty, 15)
self.assertEqual(productA.qty_available, 15)