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,6 +1,7 @@
# -*- 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
@ -10,9 +11,8 @@ class TestUnbuild(TestMrpCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.stock_location = cls.env.ref('stock.stock_location_stock')
cls.env.ref('base.group_user').write({
'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]
'implied_ids': [Command.link(cls.env.ref('stock.group_production_lot').id)],
})
def test_unbuild_standart(self):
@ -87,7 +87,6 @@ class TestUnbuild(TestMrpCommon):
lot = self.env['stock.lot'].create({
'name': 'lot1',
'product_id': p_final.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
@ -96,7 +95,7 @@ class TestUnbuild(TestMrpCommon):
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.lot_producing_id = lot
mo_form.lot_producing_ids.set(lot)
mo = mo_form.save()
mo.button_mark_done()
@ -152,7 +151,7 @@ class TestUnbuild(TestMrpCommon):
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
def test_unbuild_with_comnsumed_lot(self):
def test_unbuild_with_consumed_lot(self):
""" This test creates a MO and then creates 3 unbuild
orders for the final product. Only once of the two consumed
product is tracked by lot. It checks the stock state after each
@ -164,7 +163,6 @@ class TestUnbuild(TestMrpCommon):
lot = self.env['stock.lot'].create({
'name': 'lot1',
'product_id': p1.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot)
@ -181,9 +179,10 @@ class TestUnbuild(TestMrpCommon):
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.lot_id = lot
ml.qty_done = 20
ml.quantity = 20
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.")
# Check quantity in stock before unbuild.
@ -198,7 +197,6 @@ class TestUnbuild(TestMrpCommon):
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.product_qty = 3
unbuild_order = x.save()
# This should fail since we do not provide the MO that we wanted to unbuild. (without MO we do not know which consumed lot we have to restore)
@ -208,6 +206,7 @@ class TestUnbuild(TestMrpCommon):
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have consumed 3 final product in stock')
unbuild_order.mo_id = mo.id
unbuild_order.product_qty = 3
unbuild_order.action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
@ -248,17 +247,14 @@ class TestUnbuild(TestMrpCommon):
lot_final = self.env['stock.lot'].create({
'name': 'lot_final',
'product_id': p_final.id,
'company_id': self.env.company.id,
})
lot_1 = self.env['stock.lot'].create({
'name': 'lot_consumed_1',
'product_id': p1.id,
'company_id': self.env.company.id,
})
lot_2 = self.env['stock.lot'].create({
'name': 'lot_consumed_2',
'product_id': p2.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot_1)
@ -268,17 +264,18 @@ class TestUnbuild(TestMrpCommon):
# FIXME sle: behavior change
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.lot_producing_id = lot_final
mo_form.lot_producing_ids.set(lot_final)
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.edit(0) as ml:
ml.qty_done = 5
ml.quantity = 5
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.edit(0) as ml:
ml.qty_done = 20
ml.quantity = 20
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.")
# Check quantity in stock before unbuild.
@ -311,6 +308,7 @@ class TestUnbuild(TestMrpCommon):
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.lot_id = lot_final
x.product_qty = 3
x.save().action_unbuild()
@ -322,6 +320,7 @@ class TestUnbuild(TestMrpCommon):
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.lot_id = lot_final
x.product_qty = 2
x.save().action_unbuild()
@ -333,6 +332,7 @@ class TestUnbuild(TestMrpCommon):
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.lot_id = lot_final
x.product_qty = 5
x.save().action_unbuild()
@ -350,17 +350,14 @@ class TestUnbuild(TestMrpCommon):
lot_1 = self.env['stock.lot'].create({
'name': 'lot_1',
'product_id': p2.id,
'company_id': self.env.company.id,
})
lot_2 = self.env['stock.lot'].create({
'name': 'lot_2',
'product_id': p2.id,
'company_id': self.env.company.id,
})
lot_3 = self.env['stock.lot'].create({
'name': 'lot_3',
'product_id': p2.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=lot_1)
@ -371,15 +368,8 @@ class TestUnbuild(TestMrpCommon):
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids.filtered(lambda ml: ml.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = ml.reserved_uom_qty
with details_operation_form.move_line_ids.edit(1) as ml:
ml.qty_done = ml.reserved_uom_qty
with details_operation_form.move_line_ids.edit(2) as ml:
ml.qty_done = ml.reserved_uom_qty
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.")
# Check quantity in stock before unbuild.
@ -412,64 +402,100 @@ class TestUnbuild(TestMrpCommon):
lot_1 = self.env['stock.lot'].create({
'name': 'lot_1',
'product_id': p2.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_1)
lot_finished_1 = self.env['stock.lot'].create({
'name': 'lot_finished_1',
'product_id': p_final.id,
'company_id': self.env.company.id,
})
self.assertEqual(mo.product_qty, 5)
mo_form = Form(mo)
mo_form.qty_producing = 3.0
mo_form.lot_producing_id = lot_finished_1
mo_form.lot_producing_ids.set(lot_finished_1)
mo = mo_form.save()
self.assertEqual(mo.move_raw_ids[1].quantity_done, 12)
self.assertEqual(mo.move_raw_ids[1].quantity, 12)
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.qty_done = 3
with details_operation_form.move_line_ids.edit(0) as ml:
ml.quantity = 3
ml.lot_id = lot_1
details_operation_form.save()
action = mo.button_mark_done()
backorder = Form(self.env[action['res_model']].with_context(**action['context']))
backorder.save().action_backorder()
mo.move_raw_ids.picked = True
Form.from_action(self.env, mo.button_mark_done()).save().action_backorder()
lot_2 = self.env['stock.lot'].create({
'name': 'lot_2',
'product_id': p2.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 4, lot_id=lot_2)
lot_finished_2 = self.env['stock.lot'].create({
'name': 'lot_finished_2',
'product_id': p_final.id,
'company_id': self.env.company.id,
})
mo = mo.procurement_group_id.mrp_production_ids[1]
mo = mo.production_group_id.production_ids[1]
# FIXME sle: issue in backorder?
mo.move_raw_ids.move_line_ids.unlink()
self.assertEqual(mo.product_qty, 2)
mo_form = Form(mo)
mo_form.qty_producing = 2
mo_form.lot_producing_id = lot_finished_2
mo_form.lot_producing_ids.set(lot_finished_2)
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.qty_done = 2
with details_operation_form.move_line_ids.edit(0) as ml:
ml.quantity = 2
ml.lot_id = lot_2
details_operation_form.save()
action = mo.button_mark_done()
mo1 = mo.procurement_group_id.mrp_production_ids[0]
mo1 = mo.production_group_id.production_ids[0]
ml = mo1.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_1 in m.produce_line_ids.lot_id)
self.assertEqual(sum(ml.mapped('qty_done')), 12.0, 'Should have consumed 12 for the first lot')
self.assertEqual(sum(ml.mapped('quantity')), 12.0, 'Should have consumed 12 for the first lot')
ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_2 in m.produce_line_ids.lot_id)
self.assertEqual(sum(ml.mapped('qty_done')), 8.0, 'Should have consumed 8 for the second lot')
self.assertEqual(sum(ml.mapped('quantity')), 8.0, 'Should have consumed 8 for the second lot')
def test_unbuild_without_lot_after_tracking_change(self):
""" This test creates a MO without lots, and later one of the consumed products starts being tracking by lot.
And then creates 1 unbuild order for the final product.
It checks the stock state after each order and ensure it is correct.
"""
mo, bom, p_final, p1, p2 = self.generate_mo()
self.assertEqual(len(mo), 1, 'MO should have been created')
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check quantity in stock before unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
p1.tracking = 'lot'
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.mo_id = mo
x.product_qty = 3
unbuild_order = x.save()
self.assertEqual(unbuild_order.bom_id, bom, 'Should have filled bom field automatically')
unbuild_order.action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, strict=True), 92, 'You should have 92 products in stock without lot')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
def test_unbuild_with_routes(self):
""" This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock
@ -480,22 +506,21 @@ class TestUnbuild(TestMrpCommon):
StockQuant = self.env['stock.quant']
ProductObj = self.env['product.product']
# Create new QC/Unbuild location
warehouse = self.env.ref('stock.warehouse0')
unbuild_location = self.env['stock.location'].create({
'name': 'QC/Unbuild',
'usage': 'internal',
'location_id': warehouse.view_location_id.id
'location_id': self.warehouse_1.view_location_id.id
})
# Create a product route containing a stock rule that will move product from QC/Unbuild location to stock
self.env['stock.route'].create({
'name': 'QC/Unbuild -> Stock',
'warehouse_selectable': True,
'warehouse_ids': [(4, warehouse.id)],
'rule_ids': [(0, 0, {
'warehouse_ids': [Command.link(self.warehouse_1.id)],
'rule_ids': [Command.create({
'name': 'Send Matrial QC/Unbuild -> Stock',
'action': 'push',
'picking_type_id': self.ref('stock.picking_type_internal'),
'picking_type_id': self.picking_type_int.id,
'location_src_id': unbuild_location.id,
'location_dest_id': self.stock_location.id,
})],
@ -504,15 +529,15 @@ class TestUnbuild(TestMrpCommon):
# Create a stockable product and its components
finshed_product = ProductObj.create({
'name': 'Table',
'type': 'product',
'is_storable': True,
})
component1 = ProductObj.create({
'name': 'Table head',
'type': 'product',
'is_storable': True,
})
component2 = ProductObj.create({
'name': 'Table stand',
'type': 'product',
'is_storable': True,
})
# Create bom and add components
@ -523,8 +548,8 @@ class TestUnbuild(TestMrpCommon):
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': component1.id, 'product_qty': 1}),
(0, 0, {'product_id': component2.id, 'product_qty': 1})
Command.create({'product_id': component1.id, 'product_qty': 1}),
Command.create({'product_id': component2.id, 'product_qty': 1}),
]})
# Set on hand quantity
@ -580,8 +605,8 @@ class TestUnbuild(TestMrpCommon):
self.assertEqual(picking.location_dest_id.id, self.stock_location.id, 'Wrong destination location in picking')
# Transfer it
for ml in picking.move_ids_without_package:
ml.quantity_done = 1
for ml in picking.move_ids:
ml.write({'quantity': 1, 'picked': True})
picking._action_done()
# Check the available quantity of components and final product in stock
@ -595,8 +620,7 @@ class TestUnbuild(TestMrpCommon):
- decimal accuracy of Product UoM > decimal accuracy of Units
- unbuild a product with a decimal quantity of component
"""
self.env['decimal.precision'].search([('name', '=', 'Product Unit of Measure')]).digits = 4
self.uom_unit.rounding = 0.001
self.env['decimal.precision'].search([('name', '=', 'Product Unit')]).digits = 4
self.bom_1.product_qty = 3
self.bom_1.bom_line_ids.product_qty = 5
@ -629,17 +653,16 @@ class TestUnbuild(TestMrpCommon):
"""
compo, finished = self.env['product.product'].create([{
'name': 'compo',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
}, {
'name': 'finished',
'type': 'product',
'is_storable': True,
}])
lot01, lot02 = self.env['stock.lot'].create([{
'name': n,
'product_id': compo.id,
'company_id': self.env.company.id,
} for n in ['lot01', 'lot02']])
self.env['stock.quant']._update_available_quantity(compo, self.stock_location, 1, lot_id=lot01)
self.env['stock.quant']._update_available_quantity(compo, self.stock_location, 1, lot_id=lot02)
@ -662,12 +685,13 @@ class TestUnbuild(TestMrpCommon):
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.edit(0) as ml:
ml.qty_done = 1
ml.quantity = 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.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()
uo_form = Form(self.env['mrp.unbuild'])
@ -685,7 +709,7 @@ class TestUnbuild(TestMrpCommon):
unbuild order are applied on the stock moves
"""
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
self.env.user.write({'groups_id': [(4, grp_multi_loc.id, 0)]})
self.env.user.write({'group_ids': [Command.link(grp_multi_loc.id)]})
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
prod_location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.env.user.id)])
subloc01, subloc02, = self.stock_location.child_ids[:2]
@ -706,13 +730,11 @@ class TestUnbuild(TestMrpCommon):
internal_form.picking_type_id = warehouse.int_type_id
internal_form.location_id = self.stock_location
internal_form.location_dest_id = subloc01
with internal_form.move_ids_without_package.new() as move:
with internal_form.move_ids.new() as move:
move.product_id = p_final
move.product_uom_qty = 1.0
move.quantity = 1.0
move.picked = True
internal_transfer = internal_form.save()
internal_transfer.action_confirm()
internal_transfer.action_assign()
internal_transfer.move_line_ids.qty_done = 1.0
internal_transfer.button_validate()
unbuild_order_form = Form(self.env['mrp.unbuild'])
@ -739,9 +761,8 @@ class TestUnbuild(TestMrpCommon):
order = self.env['mrp.unbuild'].create({
'product_id': self.product_4.id,
})
warehouse = self.env.ref('stock.warehouse0')
self.assertEqual(order.location_id, warehouse.lot_stock_id)
self.assertEqual(order.location_dest_id, warehouse.lot_stock_id)
self.assertEqual(order.location_id, self.stock_location)
self.assertEqual(order.location_dest_id, self.stock_location)
def test_use_unbuilt_sn_in_mo(self):
"""
@ -750,17 +771,16 @@ class TestUnbuild(TestMrpCommon):
"""
product_1 = self.env['product.product'].create({
'name': 'Product tracked by sn',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
product_1_sn = self.env['stock.lot'].create({
'name': 'sn1',
'product_id': product_1.id,
'company_id': self.env.company.id
})
component = self.env['product.product'].create({
'name': 'Product component',
'type': 'product',
'is_storable': True,
})
bom_1 = self.env['mrp.bom'].create({
'product_id': product_1.id,
@ -769,12 +789,12 @@ class TestUnbuild(TestMrpCommon):
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': component.id, 'product_qty': 1}),
Command.create({'product_id': component.id, 'product_qty': 1}),
],
})
product_2 = self.env['product.product'].create({
'name': 'finished Product',
'type': 'product',
'is_storable': True,
})
self.env['mrp.bom'].create({
'product_id': product_2.id,
@ -783,7 +803,7 @@ class TestUnbuild(TestMrpCommon):
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_1.id, 'product_qty': 1}),
Command.create({'product_id': product_1.id, 'product_qty': 1}),
],
})
# mo1
@ -795,8 +815,7 @@ class TestUnbuild(TestMrpCommon):
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 1.0
mo_form.lot_producing_id = product_1_sn
mo_form.lot_producing_ids.set(product_1_sn)
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
@ -804,6 +823,7 @@ class TestUnbuild(TestMrpCommon):
#unbuild order
unbuild_form = Form(self.env['mrp.unbuild'])
unbuild_form.mo_id = mo
unbuild_form.lot_id = product_1_sn
unbuild_form.save().action_unbuild()
#mo2
@ -814,11 +834,12 @@ class TestUnbuild(TestMrpCommon):
details_operation_form = Form(mo2.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 = product_1_sn
ml.qty_done = 1
ml.quantity = 1
details_operation_form.save()
mo_form = Form(mo2)
mo_form.qty_producing = 1
mo2 = mo_form.save()
mo2.move_raw_ids.picked = True
mo2.button_mark_done()
self.assertEqual(mo2.state, 'done', "Production order should be in done state.")
@ -829,26 +850,25 @@ class TestUnbuild(TestMrpCommon):
"""
finished_product = self.env['product.product'].create({
'name': 'Product tracked by sn',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
finished_product_sn = self.env['stock.lot'].create({
'name': 'sn1',
'product_id': finished_product.id,
'company_id': self.env.company.id
})
component = self.env['product.product'].create({
'name': 'Product component',
'type': 'product',
'is_storable': True,
})
bom_1 = 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': component.id, 'product_qty': 1}),
Command.create({'product_id': component.id, 'product_qty': 1}),
],
})
# mo_1
@ -858,15 +878,12 @@ class TestUnbuild(TestMrpCommon):
mo_form.product_qty = 1.0
mo = mo_form.save()
mo.action_confirm()
mo.qty_producing = 1.0
mo.lot_producing_id = finished_product_sn
mo.move_raw_ids.quantity_done = 1
mo.lot_producing_ids = finished_product_sn
mo.move_raw_ids.write({'quantity': 1, 'picked': True})
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# unbuild order mo_1
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()
self.assertEqual(mo.unbuild_ids.produce_line_ids[0].product_id, finished_product)
self.assertEqual(mo.unbuild_ids.produce_line_ids[0].lot_ids, finished_product_sn)
self.assertEqual(mo.unbuild_ids.produce_line_ids[1].product_id, component)
@ -877,7 +894,6 @@ class TestUnbuild(TestMrpCommon):
component_sn = self.env['stock.lot'].create({
'name': 'component-sn1',
'product_id': component.id,
'company_id': self.env.company.id
})
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=component_sn)
#mo2 with tracked component
@ -887,15 +903,13 @@ class TestUnbuild(TestMrpCommon):
mo_form.product_qty = 1.0
mo_2 = mo_form.save()
mo_2.action_confirm()
mo_2.qty_producing = 1.0
mo_2.lot_producing_id = finished_product_sn
mo_2.move_raw_ids.quantity_done = 1
mo_2.lot_producing_ids = finished_product_sn
mo_2.move_raw_ids.write({'quantity': 1, 'picked': True})
mo_2.button_mark_done()
self.assertEqual(mo_2.state, 'done', "Production order should be in done state.")
# unbuild mo_2
action = mo_2.button_unbuild()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.action_validate()
Form.from_action(self.env, action).save().action_validate()
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[0].product_id, finished_product)
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[0].lot_ids, finished_product_sn)
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[1].product_id, component)
@ -921,6 +935,7 @@ class TestUnbuild(TestMrpCommon):
mo = mo_form.save()
mo.action_confirm()
mo.move_finished_ids._do_unreserve()
mo_form = Form(mo)
mo_form.qty_producing = 4
mo = mo_form.save()
@ -941,42 +956,233 @@ class TestUnbuild(TestMrpCommon):
unbuild_order.action_unbuild()
self.assertRecordValues(unbuild_order.produce_line_ids.move_line_ids, [
# pylint: disable=bad-whitespace
{'product_id': self.bom_1.product_id.id, 'qty_done': 3},
{'product_id': self.bom_1.bom_line_ids[0].product_id.id, 'qty_done': 0.6},
{'product_id': self.bom_1.bom_line_ids[1].product_id.id, 'qty_done': 1.2},
{'product_id': self.bom_1.product_id.id, 'quantity': 3},
{'product_id': self.bom_1.bom_line_ids[0].product_id.id, 'quantity': 0.6},
{'product_id': self.bom_1.bom_line_ids[1].product_id.id, 'quantity': 1.2},
])
def test_unbuild_update_forecasted_qty(self):
def test_unbuild_less_quantity_consumed(self):
"""
Test that the unbuild correctly updates the forecasted quantity of a product.
Tests that you don't unbuild more than you consumed during production.
BoM uses component x20, but only 15 are consumed during the production order.
Unbuilding the MO should only put 15 components back in stock.
"""
bom = self.bom_4
product = bom.product_id
# qty_available + incoming_qty - outgoing_qty = virtual_available
# Currently: 0.0 + 0.0 - 0.0 = 0.0
self.assertRecordValues(product, [{"qty_available": 0.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 0.0}])
# Manufacture 20 unit
bom = self.env['mrp.bom'].create({
'product_id': self.product_2.id,
'product_tmpl_id': self.product_2.product_tmpl_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
Command.create({'product_id': self.product_3.id, 'product_qty': 20}),
]
})
with Form(self.env['mrp.production']) as mo_form:
mo_form.product_id = self.product_2
mo_form.bom_id = bom
mo_form.product_qty = 1
mo = mo_form.save()
mo.action_confirm()
mo.qty_producing = 1.0
mo.move_raw_ids.write({'quantity': 15, 'picked': True})
mo.button_mark_done()
Form.from_action(self.env, mo.button_unbuild()).save().action_validate()
self.assertEqual(mo.unbuild_ids.produce_line_ids.filtered(lambda m: m.product_id == self.product_3).product_uom_qty, 15)
def test_unbuild_mo_different_qty(self):
# Test the unbuild of a MO with qty_produced > product_qty
bom = self.env['mrp.bom'].create({
'product_id': self.product_2.id,
'product_tmpl_id': self.product_2.product_tmpl_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [Command.create({'product_id': self.product_3.id, 'product_qty': 1})]
})
with Form(self.env['mrp.production']) as mo_form:
mo_form.product_id = self.product_2
mo_form.bom_id = bom
mo_form.product_qty = 10
mo = mo_form.save()
mo.action_confirm()
mo.qty_producing = 12
mo.move_raw_ids.write({'quantity': 12, 'picked': True})
mo.button_mark_done()
unbuild_action = mo.button_unbuild()
unbuild_action['context']['default_product_qty'] = 12
Form.from_action(self.env, unbuild_action).save().action_validate()
unbuild_fns_move = mo.unbuild_ids.produce_line_ids.filtered(lambda m: m.product_id == self.product_2)
self.assertEqual(len(unbuild_fns_move), 1)
self.assertEqual(unbuild_fns_move.state, "done")
self.assertEqual(unbuild_fns_move.quantity, 12)
def test_putaway_strategy_with_unbuild(self):
"""
Test that the putaway strategy is correctly applied when unbuilding a product
"""
# Create a putaway strategy for the component product
putaway_strategy = self.env["stock.putaway.rule"].create({
"location_in_id": self.stock_location.id,
"location_out_id": self.shelf_1.id,
'product_id': self.bom_4.bom_line_ids.product_id.id,
})
# Create a MO for the finished product
mo = self.env['mrp.production'].create({
'product_id': self.bom_4.product_id.id,
'product_qty': 1.0,
'bom_id': self.bom_4.id,
'product_uom_id': self.bom_4.product_uom_id.id,
})
mo.action_confirm()
mo.qty_producing = 1.0
mo.move_raw_ids.write({'quantity': 1, 'picked': True})
mo.button_mark_done()
# Unbuild the MO and check that the putaway strategy is applied for the component product
unbuild_action = mo.button_unbuild()
unbuild_action['context']['default_product_qty'] = 1
unbuild_order = Form.from_action(self.env, unbuild_action).save()
unbuild_order.action_unbuild()
component_move_unbuild = unbuild_order.produce_line_ids.filtered(lambda m: m.product_id == self.bom_4.bom_line_ids.product_id)
self.assertEqual(component_move_unbuild.move_line_ids.location_dest_id, putaway_strategy.location_out_id)
def test_unbuild_consigned_comp(self):
""" Test that after unbuild, consigned quant still have the same owner as before the MO."""
consigned_partner = self.env['res.partner'].create({'name': 'consigned partner'})
mo, _, _, p1, _ = self.generate_mo(qty_final=1, qty_base_1=7)
self.assertEqual(len(mo), 1, 'MO should have been created')
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 3)
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 4, owner_id=consigned_partner)
mo.action_assign()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
unbuild_form = Form(self.env['mrp.unbuild'])
unbuild_form.mo_id = mo
unbuild_form.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 7)
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, owner_id=consigned_partner), 4)
def test_unbuild_non_storable_product(self):
"""Check that the move values of an unbuild of a non-storable product are correct.
"""
self.product_4.is_storable = False
self.product_3.is_storable = False
self.env['mrp.bom.byproduct'].create({
'bom_id': self.bom_1.id,
'product_id': self.product_3.id,
'product_qty': 1,
'product_uom_id': self.product_3.uom_id.id
})
# Create mo
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product
mo_form.bom_id = bom
mo_form.product_qty = 20.0
mo_form.product_id = self.product_4
mo_form.bom_id = self.bom_1
mo_form.product_uom_id = self.product_4.uom_id
mo_form.product_qty = 4.0
mo = mo_form.save()
mo.action_confirm()
self.assertRecordValues(product, [{"qty_available": 0.0, "incoming_qty": 20.0, "outgoing_qty": 0.0, "virtual_available": 20.0}])
mo.qty_producing = 20.0
mo.move_raw_ids.quantity_done = 20.0
# Produce the final product
mo_form = Form(mo)
mo_form.qty_producing = 4.0
mo_form.save()
mo.button_mark_done()
self.assertRecordValues(product, [{"qty_available": 20.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 20.0}])
# Unlock the MO and add 10 additional produced quantity
mo.action_toggle_is_locked()
with Form(mo) as mo_form:
mo_form.qty_producing = 30.0
mo = mo_form.save()
self.assertRecordValues(product, [{"qty_available": 30.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 30.0}])
# Unbuild the 15 units
action = mo.button_unbuild()
unbuild_form = Form(self.env[action['res_model']].with_context(action['context']))
unbuild_form.product_qty = 15.0
wizard = unbuild_form.save()
wizard.action_validate()
self.assertRecordValues(product, [{"qty_available": 15.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 15.0}])
unbuild_wizard = Form(self.env['mrp.unbuild'])
unbuild_wizard.mo_id = mo
unbuild = unbuild_wizard.save()
unbuild.action_unbuild()
self.assertRecordValues(unbuild.produce_line_ids, [
{'product_id': self.product_4.id, 'quantity': 4, 'state': 'done'}, # Stick
{'product_id': self.product_3.id, 'quantity': 12, 'state': 'done'}, # Stone
{'product_id': self.product_2.id, 'quantity': 24, 'state': 'done'}, # Wood
{'product_id': self.product_1.id, 'quantity': 48, 'state': 'done'}, # Courage
])
def test_unbuild_tracked_component_multiple_unbuilds_same_mo(self):
"""
Create a Manufacturing Order producing 2 units of a serial-tracked finished product
from a serial-tracked component, and verify that during two successive unbuilds,
the correct component serial numbers are restored.
- The MO produces 2 units of P1, consuming serials SN1 and SN2 of C1
- Unbuild the first P1 serial SN1 of C1 is restored
- Unbuild the second P1 serial SN2 of C1 is restored (SN1 must not be reused)
"""
(self.bom_4.product_id | self.bom_4.bom_line_ids.product_id).is_storable = True
(self.bom_4.product_id | self.bom_4.bom_line_ids.product_id).tracking = 'serial'
component = self.bom_4.bom_line_ids.product_id
# Serials for component
sn1 = self.env['stock.lot'].create({
'name': 'SN1',
'product_id': component.id,
})
sn2 = self.env['stock.lot'].create({
'name': 'SN2',
'product_id': component.id,
})
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=sn1)
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=sn2)
# Serials for finished product
fp_sn1 = self.env['stock.lot'].create({
'name': 'SN1-P1',
'product_id': self.bom_4.product_id.id,
})
fp_sn2 = self.env['stock.lot'].create({
'name': 'SN2-P1',
'product_id': self.bom_4.product_id.id,
})
mo = self.env['mrp.production'].create({
'product_id': self.bom_4.product_id.id,
'product_qty': 2.0,
'bom_id': self.bom_4.id,
'product_uom_id': self.bom_4.product_uom_id.id,
})
mo.action_confirm()
mo.lot_producing_ids = fp_sn1 + fp_sn2
mo._set_qty_producing()
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, sn1 + sn2)
unbuild_1 = self.env['mrp.unbuild'].create({
'mo_id': mo.id,
'product_id': self.bom_4.product_id.id,
'lot_id': fp_sn1.id,
})
unbuild_1.action_unbuild()
self.assertEqual(unbuild_1.state, 'done')
self.assertEqual(
self.env['stock.quant']._get_available_quantity(component, self.stock_location, lot_id=sn1),
1, 'SN1 should be restored after first unbuild'
)
unbuild_2 = self.env['mrp.unbuild'].create({
'mo_id': mo.id,
'product_id': self.bom_4.product_id.id,
'lot_id': fp_sn2.id,
})
unbuild_2.action_unbuild()
self.assertEqual(unbuild_2.state, 'done')
self.assertEqual(
self.env['stock.quant']._get_available_quantity(component, self.stock_location, lot_id=sn2),
1, 'SN2 should be restored after second unbuild'
)