Files
odoo_source/addons/mrp/tests/test_traceability.py
T
Aurélien (avd) 6c0744d672 [FIX] stock: speed up _find_delivery_ids_by_lot
Rewrite _find_delivery_ids_by_lot to reduce the number
of recursive calls by grouping the producing move lines
together before each recursive calls. This reduces
the number of search calls, speeding up the whole method.

Add a unittest to check if the delivery_ids field
of a lot recordset is properly computed.

opw-2824464

X-original-commit: e7b9f14d4a9ce4ed61451b24b567f751fb4bcbe2
Part-of: odoo/odoo#95398
2022-07-06 13:11:48 +02:00

530 lines
23 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)
def test_last_delivery_traceability(self):
"""
Suppose this structure (-> means 'produces')
1 x Subcomponent A -> 1 x Component A -> 1 x EndProduct A
All three tracked by lots. Ensure that after validating Picking A (out)
for EndProduct A, all three lots' delivery_ids are set to
Picking A.
"""
stock_location = self.env.ref('stock.stock_location_stock')
customer_location = self.env.ref('stock.stock_location_customers')
# Create the three lot-tracked products.
subcomponentA = self._create_product('lot')
componentA = self._create_product('lot')
endproductA = self._create_product('lot')
# Create production lots.
lot_subcomponentA, lot_componentA, lot_endProductA = self.env['stock.lot'].create([{
'name': 'lot %s' % product,
'product_id': product.id,
'company_id': self.env.company.id,
} for product in (subcomponentA, componentA, endproductA)])
# Create two boms, one for Component A and one for EndProduct A
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 [(endproductA, componentA), (componentA, subcomponentA)]])
self.env['stock.quant']._update_available_quantity(subcomponentA, stock_location, 1, lot_id=lot_subcomponentA)
# Produce 1 component A
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = componentA
mo_form.product_qty = 1
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo_form.lot_producing_id = lot_componentA
mo = mo_form.save()
mo.move_raw_ids[0].quantity_done = 1.0
mo.button_mark_done()
# Produce 1 endProduct A
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = endproductA
mo_form.product_qty = 1
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo_form.lot_producing_id = lot_endProductA
mo = mo_form.save()
mo.move_raw_ids[0].quantity_done = 1.0
mo.button_mark_done()
# Create out picking for EndProduct A
pickingA_out = self.env['stock.picking'].create({
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id})
moveA = self.env['stock.move'].create({
'name': 'Picking A move',
'product_id': endproductA.id,
'product_uom_qty': 1,
'product_uom': endproductA.uom_id.id,
'picking_id': pickingA_out.id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id})
# Confirm and assign pickingA
pickingA_out.action_confirm()
pickingA_out.action_assign()
# Set move_line lot_id to the mrp.production lot_producing_id
moveA.move_line_ids[0].write({
'qty_done': 1.0,
'lot_id': lot_endProductA.id,
})
# Transfer picking
pickingA_out._action_done()
# Use concat so that delivery_ids is computed in batch.
for lot in lot_subcomponentA.concat(lot_componentA, lot_endProductA):
self.assertEqual(lot.delivery_ids.ids, pickingA_out.ids)