19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:47 +01:00
parent accf5918df
commit 6e65e8c877
688 changed files with 225434 additions and 199401 deletions

View file

@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.exceptions import UserError
from datetime import datetime
import logging
from freezegun import freeze_time
from datetime import datetime
_logger = logging.getLogger(__name__)
@ -22,9 +24,8 @@ class TestTraceability(TestMrpCommon):
def _create_product(self, tracking):
return self.env['product.product'].create({
'name': 'Product %s' % tracking,
'type': 'product',
'is_storable': True,
'tracking': tracking,
'categ_id': self.env.ref('product.product_category_all').id,
})
def test_tracking_types_on_mo(self):
@ -34,37 +35,36 @@ class TestTraceability(TestMrpCommon):
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,
'location_id': self.stock_location.id,
'product_id': consumed_no_track.id,
'inventory_quantity': 3
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'location_id': self.stock_location.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
'lot_id': Lot.create({'name': 'L1', 'product_id': consumed_lot.id}).id
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'location_id': self.stock_location.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
'lot_id': Lot.create({'name': 'S1', 'product_id': consumed_serial.id}).id
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'location_id': self.stock_location.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
'lot_id': Lot.create({'name': 'S2', 'product_id': consumed_serial.id}).id
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'location_id': self.stock_location.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
'lot_id': Lot.create({'name': 'S3', 'product_id': consumed_serial.id}).id
})
quants.action_apply_inventory()
@ -72,20 +72,20 @@ class TestTraceability(TestMrpCommon):
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_uom_id': self.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}),
Command.create({'product_id': consumed_no_track.id, 'product_qty': 1}),
Command.create({'product_id': consumed_lot.id, 'product_qty': 1}),
Command.create({'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_uom_id = self.uom_unit
mo_form.product_qty = 1
mo = mo_form.save()
mo.action_confirm()
@ -93,19 +93,19 @@ class TestTraceability(TestMrpCommon):
# 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_form.lot_producing_ids.set(self.env['stock.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.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
ml.quantity = 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
ml.quantity = 1
details_operation_form.save()
mo.move_raw_ids.picked = True
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
@ -143,27 +143,27 @@ class TestTraceability(TestMrpCommon):
def test_tracking_on_byproducts(self):
product_final = self.env['product.product'].create({
'name': 'Finished Product',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
product_1 = self.env['product.product'].create({
'name': 'Raw 1',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
product_2 = self.env['product.product'].create({
'name': 'Raw 2',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
byproduct_1 = self.env['product.product'].create({
'name': 'Byproduct 1',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
byproduct_2 = self.env['product.product'].create({
'name': 'Byproduct 2',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
bom_1 = self.env['mrp.bom'].create({
@ -174,12 +174,12 @@ class TestTraceability(TestMrpCommon):
'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})
Command.create({'product_id': product_1.id, 'product_qty': 1}),
Command.create({'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})
Command.create({'product_id': byproduct_1.id, 'product_qty': 1, 'product_uom_id': byproduct_1.uom_id.id}),
Command.create({'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
@ -188,124 +188,104 @@ class TestTraceability(TestMrpCommon):
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.lot_producing_id = self.env['stock.lot'].create({
mo.lot_producing_ids = self.env['stock.lot'].create({
'product_id': product_final.id,
'name': 'Final_lot_1',
'company_id': self.env.company.id,
})
mo = mo_form.save()
mo.qty_producing = 1
mo.set_qty_producing()
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:
with details_operation_form.move_line_ids.edit(0) 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:
with details_operation_form.move_line_ids.edit(0) 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:
with details_operation_form.move_line_ids.edit(0) 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:
with details_operation_form.move_line_ids.edit(0) 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()
mo.move_raw_ids.picked = True
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_backorder = mo.production_group_id.production_ids[-1]
mo_form = Form(mo_backorder)
mo_form.lot_producing_id = self.env['stock.lot'].create({
mo_form.lot_producing_ids.set(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:
with details_operation_form.move_line_ids.edit(0) 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:
with details_operation_form.move_line_ids.edit(0) 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:
with details_operation_form.move_line_ids.edit(0) 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:
with details_operation_form.move_line_ids.edit(0) 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.move_raw_ids.picked = True
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')
@ -315,19 +295,19 @@ class TestTraceability(TestMrpCommon):
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)
self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.quantity), 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)
self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids.filtered(lambda l: l.quantity), 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)
self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.quantity), 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)
@ -338,25 +318,24 @@ class TestTraceability(TestMrpCommon):
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)
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1)
self.env['stock.quant']._update_available_quantity(p2, self.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_form.lot_producing_ids.set(lot)
mo = mo_form.save()
mo.move_raw_ids.picked = True
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'])
@ -366,33 +345,28 @@ class TestTraceability(TestMrpCommon):
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_form.lot_producing_ids.set(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.move_raw_ids.picked = True
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
def test_tracked_and_manufactured_component(self):
"""
Suppose this structure:
""" 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')
picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
picking_type.use_auto_consume_components_lots = True
productA, productB, productC = self.env['product.product'].create([{
'name': 'Product A',
'type': 'product',
'is_storable': True,
}, {
'name': 'Product B',
'type': 'product',
'is_storable': True,
'tracking': 'lot',
}, {
'name': 'Product C',
@ -402,7 +376,6 @@ class TestTraceability(TestMrpCommon):
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([{
@ -411,11 +384,11 @@ class TestTraceability(TestMrpCommon):
'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})],
'bom_line_ids': [Command.create({'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)
self.env['stock.quant']._update_available_quantity(productB, self.stock_location, 10, lot_id=lot_B01)
self.env['stock.quant']._update_available_quantity(productB, self.stock_location, 5, lot_id=lot_B02)
# Produce 15 x productA
mo_form = Form(self.env['mrp.production'])
@ -423,10 +396,7 @@ class TestTraceability(TestMrpCommon):
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()
mo.button_mark_done()
# Produce 15 x productB
mo_form = Form(self.env['mrp.production'])
@ -436,8 +406,9 @@ class TestTraceability(TestMrpCommon):
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 15
mo_form.lot_producing_id = lot_B03
mo_form.lot_producing_ids.set(lot_B03)
mo = mo_form.save()
mo.lot_producing_ids = lot_B03
mo.button_mark_done()
self.assertEqual(lot_B01.product_qty, 0)
@ -453,10 +424,6 @@ class TestTraceability(TestMrpCommon):
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')
@ -466,7 +433,6 @@ class TestTraceability(TestMrpCommon):
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
@ -476,10 +442,10 @@ class TestTraceability(TestMrpCommon):
'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})],
'bom_line_ids': [Command.create({'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)
self.env['stock.quant']._update_available_quantity(subcomponentA, self.stock_location, 1, lot_id=lot_subcomponentA)
# Produce 1 component A
mo_form = Form(self.env['mrp.production'])
@ -488,10 +454,9 @@ class TestTraceability(TestMrpCommon):
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo_form.lot_producing_id = lot_componentA
mo_form.lot_producing_ids.set(lot_componentA)
mo = mo_form.save()
mo.move_raw_ids[0].quantity_done = 1.0
mo.move_raw_ids.picked = True
mo.button_mark_done()
# Produce 1 endProduct A
@ -501,37 +466,34 @@ class TestTraceability(TestMrpCommon):
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo_form.lot_producing_id = lot_endProductA
mo_form.lot_producing_ids.set(lot_endProductA)
mo = mo_form.save()
mo.move_raw_ids[0].quantity_done = 1.0
mo.move_raw_ids[0].write({'quantity': 1.0, 'picked': True})
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})
'picking_type_id': self.picking_type_out.id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
})
moveA = self.env['stock.move'].create({
'name': 'Picking A move',
'product_id': endproductA.id,
'product_uom_qty': 1,
'quantity': 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()
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
})
# Set move_line lot_id to the mrp.production lot_producing_id
moveA.move_line_ids[0].write({
'qty_done': 1.0,
'quantity': 1.0,
'lot_id': lot_endProductA.id,
})
# Transfer picking
moveA.picked = True
pickingA_out._action_done()
# Use concat so that delivery_ids is computed in batch.
@ -544,20 +506,16 @@ class TestTraceability(TestMrpCommon):
Build a product P that uses C with SN, unbuild P, scrap SN, unscrap SN
and rebuild a product with SN in the components
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
stock_location = warehouse.lot_stock_id
component = self.bom_4.bom_line_ids.product_id
component.write({
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
serial_number = self.env['stock.lot'].create({
'product_id': component.id,
'name': 'Super Serial',
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=serial_number)
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=serial_number)
# produce 1
mo_form = Form(self.env['mrp.production'])
@ -569,18 +527,17 @@ class TestTraceability(TestMrpCommon):
with Form(mo) as mo_form:
mo_form.qty_producing = 1
mo.move_raw_ids.move_line_ids.qty_done = 1
mo.move_raw_ids.move_line_ids.quantity = 1
mo.button_mark_done()
# unbuild
action = mo.button_unbuild()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.action_validate()
Form.from_action(self.env, mo.button_unbuild()).save().action_validate()
# scrap the component
scrap = self.env['stock.scrap'].create({
'product_id': component.id,
'product_uom_id': component.uom_id.id,
'location_id': self.stock_location.id,
'scrap_qty': 1,
'lot_id': serial_number.id,
})
@ -589,18 +546,18 @@ class TestTraceability(TestMrpCommon):
# unscrap the component
internal_move = self.env['stock.move'].create({
'name': component.name,
'location_id': scrap_location.id,
'location_dest_id': stock_location.id,
'location_dest_id': self.stock_location.id,
'product_id': component.id,
'product_uom': component.uom_id.id,
'product_uom_qty': 1.0,
'move_line_ids': [(0, 0, {
'picked': True,
'move_line_ids': [Command.create({
'product_id': component.id,
'location_id': scrap_location.id,
'location_dest_id': stock_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': component.uom_id.id,
'qty_done': 1.0,
'quantity': 1.0,
'lot_id': serial_number.id,
})],
})
@ -617,12 +574,13 @@ class TestTraceability(TestMrpCommon):
with Form(mo) as mo_form:
mo_form.qty_producing = 1
mo.move_raw_ids.move_line_ids.qty_done = 1
mo.move_raw_ids.move_line_ids.quantity = 1
mo.move_raw_ids.picked = True
mo.button_mark_done()
self.assertRecordValues((mo.move_finished_ids + mo.move_raw_ids).move_line_ids, [
{'product_id': self.bom_4.product_id.id, 'lot_id': False, 'qty_done': 1},
{'product_id': component.id, 'lot_id': serial_number.id, 'qty_done': 1},
{'product_id': self.bom_4.product_id.id, 'lot_id': False, 'quantity': 1},
{'product_id': component.id, 'lot_id': serial_number.id, 'quantity': 1},
])
def test_generate_serial_button(self):
@ -633,30 +591,150 @@ class TestTraceability(TestMrpCommon):
# generate lot lot_0 on MO
mo.action_generate_serial()
lot_0 = mo.lot_producing_id.name
lot_0 = mo.lot_producing_ids[:1].name
# manually create lot_1 (lot_0 + 1)
lot_1 = self.env['stock.lot'].create({
'name': str(int(lot_0) + 1).zfill(7),
'product_id': p_final.id,
'company_id': self.env.company.id,
}).name
# generate lot lot_2 on a new MO
mo = mo.copy()
mo.action_confirm()
mo.action_generate_serial()
lot_2 = mo.lot_producing_id.name
lot_2 = mo.lot_producing_ids.name
self.assertEqual(lot_2, str(int(lot_1) + 1).zfill(7))
def test_generate_serial_button_sequence(self):
"""Test if serial in form "00000dd" is manually created, the generate serial
correctly create new serial from sequence.
"""
mo, _bom, p_final, _p1, _p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='serial')
p_final.serial_prefix_format = 'TEST/'
# manually create lot_1
self.env['stock.lot'].create({
'name': 'TEST/0000001',
'product_id': p_final.id,
})
# generate serial lot_2 from the MO (next_serial)
mo.action_generate_serial()
self.assertEqual(mo.lot_producing_ids[:1].name, "TEST/0000002")
p_final.lot_sequence_id.prefix = 'xx%(doy)sxx'
# generate serial lot_3 from the MO (next from sequence)
mo.lot_producing_ids = self.env['stock.lot']
mo.action_generate_serial()
self.assertIn(datetime.now().strftime('%j'), mo.lot_producing_ids.name)
def test_use_customized_serial_sequence(self):
"""
Test that serial numbers are generated with the correct prefix and sequence,
that manually provided serial numbers are correctly applied, and that
serial numbering remains consistent across multiple manufacturing orders.
"""
mo, bom, final_product, _comp_1, _comp_2 = self.generate_mo(
tracking_final='serial',
qty_base_1=1,
qty_base_2=1,
qty_final=2,
)
final_product.lot_sequence_id.prefix = 'TEST'
final_product.lot_sequence_id.number_next_actual = 1
serials_wizard = Form.from_action(self.env, mo.action_generate_serial())
self.assertEqual(serials_wizard.lot_name, 'TEST0000001')
serials_wizard.save().action_generate_serial_numbers()
serials_wizard.save().action_apply()
self.assertRecordValues(mo.lot_producing_ids.sorted('name'), [
{'name': 'TEST0000001'},
{'name': 'TEST0000002'},
])
mo.button_mark_done()
self.assertEqual(self.env['stock.quant']._get_available_quantity(final_product, self.stock_location), 2)
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', final_product.id)]).sorted('name'), [
{'name': 'TEST0000001'},
{'name': 'TEST0000002'},
])
self.assertEqual(final_product.serial_prefix_format + final_product.next_serial, 'TEST0000003')
second_mo = self.env['mrp.production'].create({
'product_id': final_product.id,
'bom_id': bom.id,
'product_qty': 5,
})
second_mo.action_confirm()
second_serials_wizard = Form.from_action(self.env, second_mo.action_generate_serial())
self.assertEqual(second_serials_wizard.lot_name, 'TEST0000003')
second_serials_wizard.serial_numbers = 'TEST0000005\nLOREM002\nTEST0000003\nIPSUM101\nTEST0000004'
second_serials_wizard.save().action_apply()
self.assertRecordValues(second_mo.lot_producing_ids.sorted('name'), [
{'name': 'IPSUM101'},
{'name': 'LOREM002'},
{'name': 'TEST0000003'},
{'name': 'TEST0000004'},
{'name': 'TEST0000005'},
])
second_mo.button_mark_done()
self.assertEqual(self.env['stock.quant']._get_available_quantity(final_product, self.stock_location), 2 + 5)
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', final_product.id)]).sorted('name'), [
{'name': 'IPSUM101'},
{'name': 'LOREM002'},
{'name': 'TEST0000001'},
{'name': 'TEST0000002'},
{'name': 'TEST0000003'},
{'name': 'TEST0000004'},
{'name': 'TEST0000005'},
])
third_mo = self.env['mrp.production'].create({
'product_id': final_product.id,
'bom_id': bom.id,
'product_qty': 2,
})
third_mo.action_confirm()
third_serials_wizard = Form.from_action(self.env, third_mo.action_generate_serial())
self.assertEqual(third_serials_wizard.lot_name, 'TEST0000006')
@freeze_time('2024-02-03')
def test_interpolation_in_batch_serials(self):
"""
Test that prefixes are correctly interpolated when
generating multiple serial numbers in one MO
"""
mo, _bom, final_product, _comp_1, _comp_2 = self.generate_mo(
tracking_final='serial',
qty_base_1=1,
qty_base_2=1,
qty_final=2,
)
final_product.lot_sequence_id.prefix = '%(day)s-%(month)s-'
final_product.lot_sequence_id.number_next_actual = 1
serials_wizard = Form.from_action(self.env, mo.action_generate_serial())
self.assertEqual(serials_wizard.lot_name, '03-02-0000001')
serials_wizard.save().action_generate_serial_numbers()
serials_wizard.save().action_apply()
self.assertRecordValues(mo.lot_producing_ids.sorted('name'), [
{'name': '03-02-0000001'},
{'name': '03-02-0000002'},
])
mo.button_mark_done()
self.assertEqual(self.env['stock.quant']._get_available_quantity(final_product, self.stock_location), 2)
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', final_product.id)]).sorted('name'), [
{'name': '03-02-0000001'},
{'name': '03-02-0000002'},
])
self.assertEqual(final_product.next_serial, '0000003')
def test_assign_stock_move_date_on_mark_done(self):
product_final = self.env['product.product'].create({
'name': 'Finished Product',
'type': 'product',
'is_storable': True,
})
with freeze_time('2024-01-15'):
production = self.env['mrp.production'].create({
'product_id': product_final.id,
'product_qty': 1,
'date_planned_start': datetime(2024, 1, 10)
'date_start': datetime(2024, 1, 10)
})
production.action_confirm()
production.qty_producing = 1
@ -670,10 +748,9 @@ class TestTraceability(TestMrpCommon):
Consume SN
Consume SN -> Should raise an error as it has already been consumed
"""
stock_location = self.env.ref('stock.stock_location_stock')
component = self.bom_4.bom_line_ids.product_id
component.write({
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
@ -682,7 +759,7 @@ class TestTraceability(TestMrpCommon):
'name': name,
'company_id': self.env.company.id,
} for name in ['SN01', 'SN02']])
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=sn_lot02)
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=sn_lot02)
mo = self.env['mrp.production'].create({
'product_id': component.id,
@ -691,11 +768,10 @@ class TestTraceability(TestMrpCommon):
'company_id': self.env.company.id,
})
mo.action_confirm()
mo.qty_producing = 1
mo.lot_producing_id = sn_lot01
mo.lot_producing_ids = sn_lot01
mo.button_mark_done()
self.assertRecordValues(mo.move_finished_ids.move_line_ids, [
{'product_id': component.id, 'lot_id': sn_lot01.id, 'qty_done': 1.0, 'state': 'done'},
{'product_id': component.id, 'lot_id': sn_lot01.id, 'quantity': 1.0, 'state': 'done'},
])
mo_form = Form(self.env['mrp.production'])
@ -703,11 +779,12 @@ class TestTraceability(TestMrpCommon):
mo = mo_form.save()
mo.action_confirm()
mo.qty_producing = 1
mo.move_raw_ids.move_line_ids.qty_done = 1
mo.move_raw_ids.move_line_ids.quantity = 1
mo.move_raw_ids.move_line_ids.lot_id = sn_lot01
mo.move_raw_ids.move_line_ids.picked = True
mo.button_mark_done()
self.assertRecordValues(mo.move_raw_ids.move_line_ids, [
{'product_id': component.id, 'lot_id': sn_lot01.id, 'qty_done': 1.0, 'state': 'done'},
{'product_id': component.id, 'lot_id': sn_lot01.id, 'quantity': 1.0, 'state': 'done'},
])
mo_form = Form(self.env['mrp.production'])
@ -715,8 +792,9 @@ class TestTraceability(TestMrpCommon):
mo = mo_form.save()
mo.action_confirm()
mo.qty_producing = 1
mo.move_raw_ids.move_line_ids.qty_done = 1
mo.move_raw_ids.move_line_ids.quantity = 1
mo.move_raw_ids.move_line_ids.lot_id = sn_lot01
mo.move_raw_ids.move_line_ids.picked = True
with self.assertRaises(UserError):
mo.button_mark_done()
@ -730,7 +808,7 @@ class TestTraceability(TestMrpCommon):
"""
component = self.bom_4.bom_line_ids.product_id
component.write({
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
@ -747,8 +825,7 @@ class TestTraceability(TestMrpCommon):
'company_id': self.env.company.id,
})
mo_produce_sn.action_confirm()
mo_produce_sn.qty_producing = 1
mo_produce_sn.lot_producing_id = sn
mo_produce_sn.lot_producing_ids = sn
mo_produce_sn.button_mark_done()
mo_consume_sn_form = Form(self.env['mrp.production'])
@ -756,11 +833,12 @@ class TestTraceability(TestMrpCommon):
mo_consume_sn = mo_consume_sn_form.save()
mo_consume_sn.action_confirm()
mo_consume_sn.qty_producing = 1
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
mo_consume_sn.move_raw_ids.move_line_ids.quantity = 1
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
mo_consume_sn.move_raw_ids.move_line_ids.picked = True
mo_consume_sn.button_mark_done()
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
{'product_id': component.id, 'lot_id': sn.id, 'quantity': 1.0, 'state': 'done'},
])
unbuild_form = Form(self.env['mrp.unbuild'])
@ -772,11 +850,12 @@ class TestTraceability(TestMrpCommon):
mo_consume_sn = mo_consume_sn_form.save()
mo_consume_sn.action_confirm()
mo_consume_sn.qty_producing = 1
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
mo_consume_sn.move_raw_ids.move_line_ids.quantity = 1
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
mo_consume_sn.move_raw_ids.move_line_ids.picked = True
mo_consume_sn.button_mark_done()
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
{'product_id': component.id, 'lot_id': sn.id, 'quantity': 1.0, 'state': 'done'},
])
def test_produce_consume_unbuild_all_and_consume(self):
@ -789,10 +868,9 @@ class TestTraceability(TestMrpCommon):
Consume SN
-> We should not raise any UserError
"""
stock_location = self.env.ref('stock.stock_location_stock')
component = self.bom_4.bom_line_ids.product_id
component.write({
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
@ -809,8 +887,7 @@ class TestTraceability(TestMrpCommon):
'company_id': self.env.company.id,
})
mo_produce_sn.action_confirm()
mo_produce_sn.qty_producing = 1
mo_produce_sn.lot_producing_id = sn
mo_produce_sn.lot_producing_ids = sn
mo_produce_sn.button_mark_done()
mo_consume_sn_form = Form(self.env['mrp.production'])
@ -818,11 +895,12 @@ class TestTraceability(TestMrpCommon):
mo_consume_sn = mo_consume_sn_form.save()
mo_consume_sn.action_confirm()
mo_consume_sn.qty_producing = 1
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
mo_consume_sn.move_raw_ids.move_line_ids.quantity = 1
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
mo_consume_sn.move_raw_ids.move_line_ids.picked = True
mo_consume_sn.button_mark_done()
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
{'product_id': component.id, 'lot_id': sn.id, 'quantity': 1.0, 'state': 'done'},
])
unbuild_form = Form(self.env['mrp.unbuild'])
@ -831,18 +909,20 @@ class TestTraceability(TestMrpCommon):
unbuild_form = Form(self.env['mrp.unbuild'])
unbuild_form.mo_id = mo_produce_sn
unbuild_form.lot_id = sn
unbuild_form.save().action_unbuild()
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=sn)
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=sn)
mo_consume_sn_form = Form(self.env['mrp.production'])
mo_consume_sn_form.bom_id = self.bom_4
mo_consume_sn = mo_consume_sn_form.save()
mo_consume_sn.action_confirm()
mo_consume_sn.qty_producing = 1
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
mo_consume_sn.move_raw_ids.move_line_ids.quantity = 1
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
mo_consume_sn.move_raw_ids.move_line_ids.picked = True
mo_consume_sn.button_mark_done()
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
{'product_id': component.id, 'lot_id': sn.id, 'quantity': 1.0, 'state': 'done'},
])