Initial commit: Mrp packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 50d736b3bd
739 changed files with 538193 additions and 0 deletions

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from . import test_bom
from . import test_byproduct
from . import test_cancel_mo
from . import test_order
from . import test_stock
from . import test_stock_report
from . import test_warehouse_multistep_manufacturing
from . import test_procurement
from . import test_unbuild
from . import test_oee
from . import test_traceability
from . import test_multicompany
from . import test_backorder
from . import test_smp
from . import test_performance
from . import test_consume_tracked_component
from . import test_manual_consumption

View file

@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
from odoo.tests import Form
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.stock.tests import common2
class TestMrpCommon(common2.TestStockCommon):
@classmethod
def generate_mo(cls, tracking_final='none', tracking_base_1='none', tracking_base_2='none', qty_final=5, qty_base_1=4, qty_base_2=1, picking_type_id=False, consumption=False):
""" This function generate a manufacturing order with one final
product and two consumed product. Arguments allows to choose
the tracking/qty for each different products. It returns the
MO, used bom and the tree products.
"""
product_to_build = cls.env['product.product'].create({
'name': 'Young Tom',
'type': 'product',
'tracking': tracking_final,
})
product_to_use_1 = cls.env['product.product'].create({
'name': 'Botox',
'type': 'product',
'tracking': tracking_base_1,
})
product_to_use_2 = cls.env['product.product'].create({
'name': 'Old Tom',
'type': 'product',
'tracking': tracking_base_2,
})
bom_1 = cls.env['mrp.bom'].create({
'product_id': product_to_build.id,
'product_tmpl_id': product_to_build.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 1.0,
'type': 'normal',
'consumption': consumption if consumption else 'flexible',
'bom_line_ids': [
(0, 0, {'product_id': product_to_use_2.id, 'product_qty': qty_base_2}),
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': qty_base_1})
]})
mo_form = Form(cls.env['mrp.production'])
mo_form.product_id = product_to_build
if picking_type_id:
mo_form.picking_type_id = picking_type_id
mo_form.bom_id = bom_1
mo_form.product_qty = qty_final
mo = mo_form.save()
mo.action_confirm()
return mo, bom_1, product_to_build, product_to_use_1, product_to_use_2
@classmethod
def setUpClass(cls):
super(TestMrpCommon, cls).setUpClass()
(
cls.product_4,
cls.product_5,
cls.product_6,
cls.product_8,
) = cls.env['product.product'].create([{
'name': 'Stick', # product_4
'uom_id': cls.uom_dozen.id,
'uom_po_id': cls.uom_dozen.id,
}, {
'name': 'Stone Tools', # product_5
}, {
'name': 'Door', # product_6
}, {
'name': 'House', # product_8
}])
# Update demo products
(cls.product_2 | cls.product_3 | cls.product_4 | cls.product_5 | cls.product_6 | cls.product_7_3 | cls.product_8).write({
'type': 'product',
})
# User Data: mrp user and mrp manager
cls.user_mrp_user = mail_new_test_user(
cls.env,
name='Hilda Ferachwal',
login='hilda',
email='h.h@example.com',
notification_type='inbox',
groups='mrp.group_mrp_user, stock.group_stock_user, mrp.group_mrp_byproducts, uom.group_uom',
)
cls.user_mrp_manager = mail_new_test_user(
cls.env,
name='Gary Youngwomen',
login='gary',
email='g.g@example.com',
notification_type='inbox',
groups='mrp.group_mrp_manager, stock.group_stock_user, mrp.group_mrp_byproducts, uom.group_uom',
)
# Required for `product_uom_id` to be visible in the view
# This class is used by a lot of tests which sets `product_uom_id` on `mrp.production`
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
cls.workcenter_1 = cls.env['mrp.workcenter'].create({
'name': 'Nuclear Workcenter',
'default_capacity': 2,
'time_start': 10,
'time_stop': 5,
'time_efficiency': 80,
})
cls.workcenter_2 = cls.env['mrp.workcenter'].create({
'name': 'Simple Workcenter',
'default_capacity': 1,
'time_start': 0,
'time_stop': 0,
'time_efficiency': 100,
})
cls.workcenter_3 = cls.env['mrp.workcenter'].create({
'name': 'Double Workcenter',
'default_capacity': 2,
'time_start': 0,
'time_stop': 0,
'time_efficiency': 100,
})
cls.bom_1 = cls.env['mrp.bom'].create({
'product_id': cls.product_4.id,
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 4.0,
'consumption': 'flexible',
'operation_ids': [
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
]})
cls.bom_2 = cls.env['mrp.bom'].create({
'product_id': cls.product_5.id,
'product_tmpl_id': cls.product_5.product_tmpl_id.id,
'product_uom_id': cls.product_5.uom_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'operation_ids': [
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
],
'type': 'phantom',
'sequence': 2,
'bom_line_ids': [
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 2}),
(0, 0, {'product_id': cls.product_3.id, 'product_qty': 3})
]})
cls.bom_3 = cls.env['mrp.bom'].create({
'product_id': cls.product_6.id,
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
'product_uom_id': cls.uom_dozen.id,
'ready_to_produce': 'asap',
'consumption': 'flexible',
'product_qty': 2.0,
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
(0, 0, {'name': 'Weld Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.product_5.id, 'product_qty': 2}),
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}),
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 12})
]})
cls.bom_4 = cls.env['mrp.bom'].create({
'product_id': cls.product_6.id,
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'operation_ids': [
(0, 0, {'name': 'Rub it gently with a cloth', 'workcenter_id': cls.workcenter_2.id,
'time_mode_batch': 1, 'time_mode': "auto", 'sequence': 1}),
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
]})
cls.bom_5 = cls.env['mrp.bom'].create({
'product_id': cls.product_6.id,
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'operation_ids': [
(0, 0, {'name': 'Rub it gently with a cloth two at once', 'workcenter_id': cls.workcenter_3.id,
'time_mode_batch': 2, 'time_mode': "auto", 'sequence': 1}),
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
]})
cls.bom_6 = cls.env['mrp.bom'].create({
'product_id': cls.product_6.id,
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'operation_ids': [
(0, 0, {'name': 'Rub it gently with a cloth two at once', 'workcenter_id': cls.workcenter_3.id,
'time_mode_batch': 1, 'time_mode': "auto", 'sequence': 1}),
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
]})
cls.stock_location_14 = cls.env['stock.location'].create({
'name': 'Shelf 2',
'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
})
cls.stock_location_components = cls.env['stock.location'].create({
'name': 'Shelf 1',
'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
})
cls.laptop = cls.env['product.product'].create({
'name': 'Acoustic Bloc Screens',
'uom_id': cls.env.ref("uom.product_uom_unit").id,
'uom_po_id': cls.env.ref("uom.product_uom_unit").id,
'type': 'product',
'tracking': 'none',
'categ_id': cls.env.ref('product.product_category_all').id,
})
cls.graphics_card = cls.env['product.product'].create({
'name': 'Individual Workplace',
'uom_id': cls.env.ref("uom.product_uom_unit").id,
'uom_po_id': cls.env.ref("uom.product_uom_unit").id,
'type': 'product',
'tracking': 'none',
'categ_id': cls.env.ref('product.product_category_all').id,
})
@classmethod
def make_prods(cls, n):
return [
cls.env["product.product"].create(
{"name": f"p{k + 1}", "type": "product"}
)
for k in range(n)
]
@classmethod
def make_bom(cls, p, *cs):
return cls.env["mrp.bom"].create(
{
"product_tmpl_id": p.product_tmpl_id.id,
"product_id": p.id,
"product_qty": 1,
"type": "phantom",
"product_uom_id": cls.uom_unit.id,
"bom_line_ids": [
(0, 0, {
"product_id": c.id,
"product_qty": 1,
"product_uom_id": cls.uom_unit.id
})
for c in cs
],
}
)

View file

@ -0,0 +1,239 @@
import copy
from odoo.exceptions import UserError
from odoo.tests import common, Form
from odoo.tools import float_is_zero
class TestConsumeTrackedComponentCommon(common.TransactionCase):
@classmethod
def setUpClass(cls):
"""
The following variables are used in each test to define the number of MO to generate.
They're also used as a verification in the executeConsumptionTriggers() to see if enough MO were passed to it
in order to test all the triggers.
SERIAL : MO's product_tracking is 'serial'
DEFAULT : MO's product_tracking is 'none' or 'lot'
AVAILABLE : MO'S raw components are fully available
"""
super().setUpClass()
cls.SERIAL_AVAILABLE_TRIGGERS_COUNT = 3
cls.DEFAULT_AVAILABLE_TRIGGERS_COUNT = 2
cls.SERIAL_TRIGGERS_COUNT = 2
cls.DEFAULT_TRIGGERS_COUNT = 1
cls.manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture')
cls.stock_id = cls.env.ref('stock.stock_location_stock').id
cls.picking_type = cls.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
cls.picking_type.use_create_components_lots = True
cls.picking_type.use_auto_consume_components_lots = True
#Create Products & Components
cls.produced_lot = cls.env['product.product'].create({
'name': 'Produced Lot',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'tracking' : 'lot',
'route_ids': [(4, cls.manufacture_route.id, 0)],
})
cls.produced_serial = cls.env['product.product'].create({
'name': 'Produced Serial',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'tracking' : 'serial',
'route_ids': [(4, cls.manufacture_route.id, 0)],
})
cls.produced_none = cls.env['product.product'].create({
'name': 'Produced None',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'tracking' : 'none',
'route_ids': [(4, cls.manufacture_route.id, 0)],
})
cls.raw_lot = cls.env['product.product'].create({
'name': 'Raw Lot',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'tracking' : 'lot',
})
cls.raw_serial = cls.env['product.product'].create({
'name': 'Raw Serial',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'tracking' : 'serial',
})
cls.raw_none = cls.env['product.product'].create({
'name': 'Raw None',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'tracking' : 'none',
})
cls.raws = [cls.raw_none, cls.raw_lot, cls.raw_serial]
#Workcenter
cls.workcenter = cls.env['mrp.workcenter'].create({
'name' : 'Assembly Line',
})
#BoMs
cls.bom_none = cls.env['mrp.bom'].create({
'product_tmpl_id' : cls.produced_none.product_tmpl_id.id,
'product_uom_id' : cls.produced_none.uom_id.id,
'consumption' : 'flexible',
'sequence' : 1
})
cls.bom_none_lines = cls.create_bom_lines(cls.bom_none, cls.raws, [3, 2, 1])
cls.bom_lot = cls.env['mrp.bom'].create({
'product_tmpl_id' : cls.produced_lot.product_tmpl_id.id,
'product_uom_id' : cls.produced_lot.uom_id.id,
'consumption' : 'flexible',
'sequence' : 2
})
cls.bom_lot_lines = cls.create_bom_lines(cls.bom_lot, cls.raws, [3, 2, 1])
cls.bom_serial = cls.env['mrp.bom'].create({
'product_tmpl_id' : cls.produced_serial.product_tmpl_id.id,
'product_uom_id' : cls.produced_serial.uom_id.id,
'consumption' : 'flexible',
'sequence' : 1
})
cls.bom_serial_lines = cls.create_bom_lines(cls.bom_serial, cls.raws, [3, 2, 1])
#Manufacturing Orders
cls.mo_none_tmpl = {
'product_id' : cls.produced_none.id,
'product_uom_id' : cls.produced_none.uom_id.id,
'product_qty' : 1,
'bom_id' : cls.bom_none.id
}
cls.mo_lot_tmpl = {
'product_id' : cls.produced_lot.id,
'product_uom_id' : cls.produced_lot.uom_id.id,
'product_qty' : 1,
'bom_id' : cls.bom_lot.id
}
cls.mo_serial_tmpl = {
'product_id' : cls.produced_serial.id,
'product_uom_id' : cls.produced_serial.uom_id.id,
'product_qty' : 1,
'bom_id' : cls.bom_serial.id
}
@classmethod
def create_quant(cls, product, qty, offset=0, name="L"):
i = 1
if product.tracking == 'serial':
i, qty = qty, 1
if name == "L":
name = "S"
vals = []
for x in range(1, i+1):
qDict = {
'location_id': cls.stock_id,
'product_id': product.id,
'inventory_quantity': qty,
}
if product.tracking != 'none':
qDict['lot_id'] = cls.env['stock.lot'].create({
'name': name + str(offset + x),
'product_id': product.id,
'company_id': cls.env.company.id
}).id
vals.append(qDict)
return cls.env['stock.quant'].create(vals)
@classmethod
def create_bom_lines(cls, bom, products, quantities=None):
if quantities is None:
quantities = [1 for i in range(len(products))]
vals = []
for product, seq in zip(products, range(len(products))):
vals.append({
'product_id' : product.id,
'product_qty' : quantities[seq],
'product_uom_id' : product.uom_id.id,
'sequence' : seq,
'bom_id' : bom.id,
})
return cls.env['mrp.bom.line'].create(vals)
@classmethod
def create_mo(cls, template, count):
vals = []
for _ in range(count):
vals.append(copy.deepcopy(template))
return cls.env['mrp.production'].create(vals)
def executeConsumptionTriggers(self, mrp_productions):
"""
There's 3 different triggers to test : _onchange_producing(), action_generate_serial(), button_mark_done().
Depending on the tracking of the final product and the availability of the components,
only a part of these 3 triggers is available or intended to work.
This function automatically call and process the appropriate triggers.
"""
tracking = mrp_productions[0].product_tracking
sameTracking = True
for mo in mrp_productions:
sameTracking = sameTracking and mo.product_tracking == tracking
self.assertTrue(sameTracking, "MOs passed to the executeConsumptionTriggers method shall have the same product_tracking")
isSerial = tracking == 'serial'
isAvailable = all(move.state == 'assigned' for move in mrp_productions.move_raw_ids)
countOk = True
length = len(mrp_productions)
if isSerial:
if isAvailable:
countOk = length == self.SERIAL_AVAILABLE_TRIGGERS_COUNT
else:
countOk = length == self.SERIAL_TRIGGERS_COUNT
else:
if isAvailable:
countOk = length == self.DEFAULT_AVAILABLE_TRIGGERS_COUNT
else:
countOk = length == self.DEFAULT_TRIGGERS_COUNT
self.assertTrue(countOk, "The number of MOs passed to the executeConsumptionTriggers method does not match the associated TRIGGERS_COUNT")
mrp_productions[0].qty_producing = mrp_productions[0].product_qty
mrp_productions[0]._onchange_producing()
i = 1
if isSerial:
mrp_productions[i].action_generate_serial()
i += 1
if isAvailable:
mark_done_action = mrp_productions[i].button_mark_done()
immediate_production_wizard = Form(
self.env['mrp.immediate.production']
.with_context(**mark_done_action['context'])
).save()
error = False
has_zero_tracked_component = not mrp_productions[i].picking_type_id.use_auto_consume_components_lots and \
any(m.state not in ['done', 'cancel'] and m.has_tracking != 'none' and float_is_zero(m.quantity_done, m.product_uom.rounding) for m in mrp_productions[i].move_raw_ids)
try:
immediate_production_wizard.process()
except UserError:
error = True
if has_zero_tracked_component:
self.assertTrue(error, "Immediate Production Wizard shall raise an error.")
else:
self.assertFalse(error, "Immediate Production Wizard shall not raise an error.")

View file

@ -0,0 +1,674 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tests import Form
from odoo.tests.common import TransactionCase
class TestMrpProductionBackorder(TestMrpCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
cls.stock_location = cls.env.ref('stock.stock_location_stock')
warehouse_form = Form(cls.env['stock.warehouse'])
warehouse_form.name = 'Test Warehouse'
warehouse_form.code = 'TWH'
cls.warehouse = warehouse_form.save()
def test_no_tracking_1(self):
"""Create a MO for 4 product. Produce 4. The backorder button should
not appear and hitting mark as done should not open the backorder wizard.
The name of the MO should be MO/001.
"""
mo = self.generate_mo(qty_final=4)[0]
mo_form = Form(mo)
mo_form.qty_producing = 4
mo = mo_form.save()
# No backorder is proposed
self.assertTrue(mo.button_mark_done())
self.assertEqual(mo._get_quantity_to_backorder(), 0)
self.assertTrue("-001" not in mo.name)
def test_no_tracking_2(self):
"""Create a MO for 4 product. Produce 1. The backorder button should
appear and hitting mark as done should open the backorder wizard. In the backorder
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
created.
The sequence of the first MO should be MO/001-01, the sequence of the second MO
should be MO/001-02.
Check that all MO are reachable through the procurement group.
"""
production, _, _, product_to_use_1, _ = self.generate_mo(qty_final=4, qty_base_1=3)
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reserve_visible, True)
# Make some stock and reserve
for product in production.move_raw_ids.product_id:
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id,
'inventory_quantity': 100,
'location_id': production.location_src_id.id,
})._apply_inventory()
production.action_assign()
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reserve_visible, False)
mo_form = Form(production)
mo_form.qty_producing = 1
production = mo_form.save()
action = production.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
# Two related MO to the procurement group
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), 2)
# Check MO backorder
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(mo_backorder.product_id.id, production.product_id.id)
self.assertEqual(mo_backorder.product_qty, 3)
self.assertEqual(sum(mo_backorder.move_raw_ids.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_uom_qty")), 9)
self.assertEqual(mo_backorder.reserve_visible, False) # the reservation of the first MO should've been moved here
def test_no_tracking_pbm_1(self):
"""Create a MO for 4 product. Produce 1. The backorder button should
appear and hitting mark as done should open the backorder wizard. In the backorder
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
created.
The sequence of the first MO should be MO/001-01, the sequence of the second MO
should be MO/001-02.
Check that all MO are reachable through the procurement group.
"""
# Required for `manufacture_steps` to be visible in the view
self.env.user.groups_id += self.env.ref("stock.group_adv_location")
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm'
production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id)
move_raw_ids = production.move_raw_ids
self.assertEqual(len(move_raw_ids), 2)
self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2})
pbm_move = move_raw_ids.move_orig_ids
self.assertEqual(len(pbm_move), 2)
self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
self.assertFalse(pbm_move.move_orig_ids)
mo_form = Form(production)
mo_form.qty_producing = 1
production = mo_form.save()
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
action = production.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(mo_backorder.delivery_count, 1)
pbm_move |= mo_backorder.move_raw_ids.move_orig_ids
# Check that quantity is correct
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
self.assertFalse(pbm_move.move_orig_ids)
def test_no_tracking_pbm_sam_1(self):
"""Create a MO for 4 product. Produce 1. The backorder button should
appear and hitting mark as done should open the backorder wizard. In the backorder
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
created.
The sequence of the first MO should be MO/001-01, the sequence of the second MO
should be MO/001-02.
Check that all MO are reachable through the procurement group.
"""
# Required for `manufacture_steps` to be visible in the view
self.env.user.groups_id += self.env.ref("stock.group_adv_location")
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id)
move_raw_ids = production.move_raw_ids
self.assertEqual(len(move_raw_ids), 2)
self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2})
pbm_move = move_raw_ids.move_orig_ids
self.assertEqual(len(pbm_move), 2)
self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
self.assertFalse(pbm_move.move_orig_ids)
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
sam_move = production.move_finished_ids.move_dest_ids
self.assertEqual(len(sam_move), 1)
self.assertEqual(sam_move.product_id.id, product_to_build.id)
self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
mo_form = Form(production)
mo_form.qty_producing = 1
production = mo_form.save()
action = production.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(mo_backorder.delivery_count, 2)
pbm_move |= mo_backorder.move_raw_ids.move_orig_ids
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
sam_move |= mo_backorder.move_finished_ids.move_orig_ids
self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
def test_tracking_backorder_series_lot_1(self):
""" Create a MO of 4 tracked products. all component is tracked by lots
Produce one by one with one bakorder for each until end.
"""
nb_product_todo = 4
production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='lot', tracking_base_1='lot', tracking_base_2='lot')
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, nb_product_todo*4, lot_id=lot_1)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, nb_product_todo, lot_id=lot_2)
production.action_assign()
active_production = production
for i in range(nb_product_todo):
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 4
ml.lot_id = lot_1
details_operation_form.save()
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.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 = 1
ml.lot_id = lot_2
details_operation_form.save()
production_form = Form(active_production)
production_form.qty_producing = 1
production_form.lot_producing_id = lot_final
active_production = production_form.save()
active_production.button_mark_done()
if i + 1 != nb_product_todo: # If last MO, don't make a backorder
action = active_production.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
active_production = active_production.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), nb_product_todo, f'You should have the {nb_product_todo} final product in stock')
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo)
def test_tracking_backorder_series_lot_2(self):
"""
Create a MO with component tracked by lots. Produce a part of the demand
by using some specific lots (not the ones suggested by the onchange).
The components' reservation of the backorder should consider which lots
have been consumed in the initial MO
"""
production, _, _, p1, p2 = self.generate_mo(tracking_base_2='lot')
lot1, lot2 = self.env['stock.lot'].create([{
'name': f'lot_consumed_{i}',
'product_id': p2.id,
'company_id': self.env.company.id,
} for i in range(2)])
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 20)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot1)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot2)
production.action_assign()
production_form = Form(production)
production_form.qty_producing = 3
details_operation_form = Form(production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 4 * 3
details_operation_form.save()
# Consume 1 Product from lot1 and 2 from lot 2
p2_smls = production.move_raw_ids.filtered(lambda m: m.product_id == p2).move_line_ids
self.assertEqual(len(p2_smls), 2, 'One for each lot')
details_operation_form = Form(production.move_raw_ids.filtered(lambda m: m.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 = 1
ml.lot_id = lot1
with details_operation_form.move_line_ids.edit(1) as ml:
ml.qty_done = 2
ml.lot_id = lot2
details_operation_form.save()
production = production_form.save()
action = production.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
p2_bo_mls = production.procurement_group_id.mrp_production_ids[-1].move_raw_ids.filtered(lambda m: m.product_id == p2).move_line_ids
self.assertEqual(len(p2_bo_mls), 1)
self.assertEqual(p2_bo_mls.lot_id, lot1)
self.assertEqual(p2_bo_mls.reserved_qty, 2)
def test_uom_backorder(self):
"""
test backorder component UoM different from the bom's UoM
"""
product_finished = self.env['product.product'].create({
'name': 'Young Tom',
'type': 'product',
})
product_component = self.env['product.product'].create({
'name': 'Botox',
'type': 'product',
'uom_id': self.env.ref('uom.product_uom_kgm').id,
'uom_po_id': self.env.ref('uom.product_uom_kgm').id,
})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_finished
mo_form.bom_id = self.env['mrp.bom'].create({
'product_id': product_finished.id,
'product_tmpl_id': product_finished.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'type': 'normal',
'consumption': 'flexible',
'bom_line_ids': [(0, 0, {
'product_id': product_component.id,
'product_qty': 1,
'product_uom_id':self.env.ref('uom.product_uom_gram').id,
}),]
})
mo_form.product_qty = 1000
mo = mo_form.save()
mo.action_confirm()
self.env['stock.quant']._update_available_quantity(product_component, self.stock_location, 1000)
mo.action_assign()
production_form = Form(mo)
production_form.qty_producing = 300
mo = production_form.save()
action = mo.button_mark_done()
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder_form.save().action_backorder()
# 300 Grams consumed and 700 reserved
self.assertAlmostEqual(self.env['stock.quant']._gather(product_component, self.stock_location).reserved_quantity, 0.7)
def test_rounding_backorder(self):
"""test backorder component rounding doesn't introduce reservation issues"""
production, _, _, p1, p2 = self.generate_mo(qty_final=5, qty_base_1=1, qty_base_2=1)
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 100)
production.action_assign()
production_form = Form(production)
production_form.qty_producing = 3.1
production = production_form.save()
details_operation_form = Form(production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 3.09
details_operation_form.save()
action = production.button_mark_done()
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder_form.save().action_backorder()
backorder = production.procurement_group_id.mrp_production_ids[-1]
# 3.09 consumed and 1.9 reserved
self.assertAlmostEqual(self.env['stock.quant']._gather(p1, self.stock_location).reserved_quantity, 1.9)
self.assertAlmostEqual(backorder.move_raw_ids.filtered(lambda m: m.product_id == p1).move_line_ids.reserved_qty, 1.9)
# Make sure we don't have an unreserve errors
backorder.do_unreserve()
def test_tracking_backorder_series_serial_1(self):
""" Create a MO of 4 tracked products (serial) with pbm_sam.
all component is tracked by serial
Produce one by one with one bakorder for each until end.
"""
nb_product_todo = 4
production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='serial', tracking_base_1='serial', tracking_base_2='serial', qty_base_1=1)
serials_final, serials_p1, serials_p2 = [], [], []
for i in range(nb_product_todo):
serials_final.append(self.env['stock.lot'].create({
'name': f'lot_final_{i}',
'product_id': p_final.id,
'company_id': self.env.company.id,
}))
serials_p1.append(self.env['stock.lot'].create({
'name': f'lot_consumed_1_{i}',
'product_id': p1.id,
'company_id': self.env.company.id,
}))
serials_p2.append(self.env['stock.lot'].create({
'name': f'lot_consumed_2_{i}',
'product_id': p2.id,
'company_id': self.env.company.id,
}))
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1, lot_id=serials_p1[-1])
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=serials_p2[-1])
production.action_assign()
active_production = production
for i in range(nb_product_todo):
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), 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.lot_id = serials_p1[i]
details_operation_form.save()
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.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 = 1
ml.lot_id = serials_p2[i]
details_operation_form.save()
production_form = Form(active_production)
production_form.qty_producing = 1
production_form.lot_producing_id = serials_final[i]
active_production = production_form.save()
active_production.button_mark_done()
if i + 1 != nb_product_todo: # If last MO, don't make a backorder
action = active_production.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
active_production = active_production.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), nb_product_todo, f'You should have the {nb_product_todo} final product in stock')
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo)
def test_tracking_backorder_immediate_production_serial_1(self):
""" Create a MO to build 2 of a SN tracked product.
Build both the starting MO and its backorder as immediate productions
(i.e. Mark As Done without setting SN/filling any quantities)
"""
mo, _, p_final, p1, p2 = self.generate_mo(qty_final=2, tracking_final='serial', qty_base_1=2, qty_base_2=2)
self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 2.0)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 2.0)
mo.action_assign()
res_dict = mo.button_mark_done()
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
immediate_wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
res_dict = immediate_wizard.process()
self.assertEqual(res_dict.get('res_model'), 'mrp.production.backorder')
backorder_wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context']))
# backorder should automatically open
action = backorder_wizard.save().action_backorder()
self.assertEqual(action.get('res_model'), 'mrp.production')
backorder_mo_form = Form(self.env[action['res_model']].with_context(action['context']).browse(action['res_id']))
backorder_mo = backorder_mo_form.save()
res_dict = backorder_mo.button_mark_done()
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
immediate_wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
immediate_wizard.process()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, "Incorrect number of final product produced.")
self.assertEqual(len(self.env['stock.lot'].search([('product_id', '=', p_final.id)])), 2, "Serial Numbers were not correctly produced.")
def test_backorder_name(self):
def produce_one(mo):
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
action = mo.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
return mo.procurement_group_id.mrp_production_ids[-1]
default_picking_type_id = self.env['mrp.production']._get_default_picking_type_id(self.env.company.id)
default_picking_type = self.env['stock.picking.type'].browse(default_picking_type_id)
mo_sequence = default_picking_type.sequence_id
mo_sequence.prefix = "WH-MO-"
initial_mo_name = mo_sequence.prefix + str(mo_sequence.number_next_actual).zfill(mo_sequence.padding)
production = self.generate_mo(qty_final=5)[0]
self.assertEqual(production.name, initial_mo_name)
backorder = produce_one(production)
self.assertEqual(production.name, initial_mo_name + "-001")
self.assertEqual(backorder.name, initial_mo_name + "-002")
backorder.backorder_sequence = 998
for seq in [998, 999, 1000]:
new_backorder = produce_one(backorder)
self.assertEqual(backorder.name, initial_mo_name + "-" + str(seq))
self.assertEqual(new_backorder.name, initial_mo_name + "-" + str(seq + 1))
backorder = new_backorder
def test_backorder_name_without_procurement_group(self):
production = self.generate_mo(qty_final=5)[0]
mo_form = Form(production)
mo_form.qty_producing = 1
mo = mo_form.save()
# Remove pg to trigger fallback on backorder name
mo.procurement_group_id = False
action = mo.button_mark_done()
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder_form.save().action_backorder()
# The pg is back
self.assertTrue(production.procurement_group_id)
backorder_ids = production.procurement_group_id.mrp_production_ids[1]
self.assertEqual(production.name.split('-')[0], backorder_ids.name.split('-')[0])
self.assertEqual(int(production.name.split('-')[1]) + 1, int(backorder_ids.name.split('-')[1]))
def test_split_draft(self):
""" test splitting a draft MO """
mo = self.env['mrp.production'].create({
'product_qty': 3,
'bom_id': self.bom_1.id,
})
self.assertEqual(mo.state, 'draft')
action = mo.action_split()
wizard = Form(self.env[action['res_model']].with_context(action['context']))
wizard.counter = 3
action = wizard.save().action_split()
def test_split_merge(self):
# Change 'Units' rounding to 1 (integer only quantities)
self.uom_unit.rounding = 1
# Create a mo for 10 products
mo, _, _, p1, p2 = self.generate_mo(qty_final=10)
# Split in 3 parts
action = mo.action_split()
wizard = Form(self.env[action['res_model']].with_context(action['context']))
wizard.counter = 3
action = wizard.save().action_split()
# Should have 3 mos
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 3)
mo1 = mo.procurement_group_id.mrp_production_ids[0]
mo2 = mo.procurement_group_id.mrp_production_ids[1]
mo3 = mo.procurement_group_id.mrp_production_ids[2]
# Check quantities
self.assertEqual(mo1.product_qty, 3)
self.assertEqual(mo2.product_qty, 3)
self.assertEqual(mo3.product_qty, 4)
# Check raw movew quantities
self.assertEqual(mo1.move_raw_ids.filtered(lambda m: m.product_id == p1).product_qty, 12)
self.assertEqual(mo2.move_raw_ids.filtered(lambda m: m.product_id == p1).product_qty, 12)
self.assertEqual(mo3.move_raw_ids.filtered(lambda m: m.product_id == p1).product_qty, 16)
self.assertEqual(mo1.move_raw_ids.filtered(lambda m: m.product_id == p2).product_qty, 3)
self.assertEqual(mo2.move_raw_ids.filtered(lambda m: m.product_id == p2).product_qty, 3)
self.assertEqual(mo3.move_raw_ids.filtered(lambda m: m.product_id == p2).product_qty, 4)
# Merge them back
expected_origin = ",".join([mo1.name, mo2.name, mo3.name])
action = (mo1 + mo2 + mo3).action_merge()
mo = self.env[action['res_model']].browse(action['res_id'])
# Check origin & initial quantity
self.assertEqual(mo.origin, expected_origin)
self.assertEqual(mo.product_qty, 10)
def test_reservation_method_w_mo(self):
""" Create a MO for 2 units, Produce 1 and create a backorder.
The MO and the backorder should be assigned according to the reservation method
defined in the default manufacturing operation type
"""
def create_mo(date_planned_start=False):
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.bom_1.product_id
mo_form.bom_id = self.bom_1
mo_form.product_qty = 2
if date_planned_start:
mo_form.date_planned_start = date_planned_start
mo = mo_form.save()
mo.action_confirm()
return mo
def produce_one(mo):
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
action = mo.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
return mo.procurement_group_id.mrp_production_ids[-1]
# Make some stock and reserve
for product in self.bom_1.bom_line_ids.product_id:
product.type = 'product'
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id,
'inventory_quantity': 100,
'location_id': self.stock_location.id,
})._apply_inventory()
default_picking_type_id = self.env['mrp.production']._get_default_picking_type_id(self.env.company.id)
default_picking_type = self.env['stock.picking.type'].browse(default_picking_type_id)
# make sure generated MO will auto-assign
default_picking_type.reservation_method = 'at_confirm'
production = create_mo()
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reserve_visible, False)
# check whether the backorder follows the same scenario as the original MO
backorder = produce_one(production)
self.assertEqual(backorder.state, 'confirmed')
self.assertEqual(backorder.reserve_visible, False)
# make sure generated MO will does not auto-assign
default_picking_type.reservation_method = 'manual'
production = create_mo()
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reserve_visible, True)
backorder = produce_one(production)
self.assertEqual(backorder.state, 'confirmed')
self.assertEqual(backorder.reserve_visible, True)
# make sure generated MO auto-assigns according to scheduled date
default_picking_type.reservation_method = 'by_date'
default_picking_type.reservation_days_before = 2
# too early for scheduled date => don't auto-assign
production = create_mo(datetime.now() + timedelta(days=10))
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reserve_visible, True)
backorder = produce_one(production)
self.assertEqual(backorder.state, 'confirmed')
self.assertEqual(backorder.reserve_visible, True)
# within scheduled date + reservation days before => auto-assign
production = create_mo()
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reserve_visible, False)
backorder = produce_one(production)
self.assertEqual(backorder.state, 'confirmed')
self.assertEqual(backorder.reserve_visible, False)
def test_split_mo(self):
"""
Test that an MO is split correctly.
BoM: 1 finished product = 0.5 comp1 + 1 comp2
"""
mo = self.env['mrp.production'].create({
'product_qty': 10,
'bom_id': self.bom_1.id,
})
self.assertEqual(mo.move_raw_ids.mapped('product_uom_qty'), [5, 10])
self.assertEqual(mo.state, 'draft')
action = mo.action_split()
wizard = Form(self.env[action['res_model']].with_context(action['context']))
wizard.counter = 10
action = wizard.save().action_split()
# check that the MO is split in 10 and the components are split accordingly
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 10)
self.assertEqual(mo.product_qty, 1)
self.assertEqual(mo.move_raw_ids.mapped('product_uom_qty'), [0.5, 1])
class TestMrpWorkorderBackorder(TransactionCase):
@classmethod
def setUpClass(cls):
super(TestMrpWorkorderBackorder, cls).setUpClass()
cls.uom_unit = cls.env['uom.uom'].search([
('category_id', '=', cls.env.ref('uom.product_uom_categ_unit').id),
('uom_type', '=', 'reference')
], limit=1)
cls.finished1 = cls.env['product.product'].create({
'name': 'finished1',
'type': 'product',
})
cls.compfinished1 = cls.env['product.product'].create({
'name': 'compfinished1',
'type': 'product',
})
cls.compfinished2 = cls.env['product.product'].create({
'name': 'compfinished2',
'type': 'product',
})
cls.workcenter1 = cls.env['mrp.workcenter'].create({
'name': 'workcenter1',
})
cls.workcenter2 = cls.env['mrp.workcenter'].create({
'name': 'workcenter2',
})
cls.bom_finished1 = cls.env['mrp.bom'].create({
'product_id': cls.finished1.id,
'product_tmpl_id': cls.finished1.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 1,
'consumption': 'flexible',
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.compfinished1.id, 'product_qty': 1}),
(0, 0, {'product_id': cls.compfinished2.id, 'product_qty': 1}),
],
'operation_ids': [
(0, 0, {'sequence': 1, 'name': 'finished operation 1', 'workcenter_id': cls.workcenter1.id}),
(0, 0, {'sequence': 2, 'name': 'finished operation 2', 'workcenter_id': cls.workcenter2.id}),
],
})
cls.bom_finished1.bom_line_ids[0].operation_id = cls.bom_finished1.operation_ids[0].id
cls.bom_finished1.bom_line_ids[1].operation_id = cls.bom_finished1.operation_ids[1].id

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,442 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.tests import common
from odoo.exceptions import ValidationError
class TestMrpByProduct(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.MrpBom = cls.env['mrp.bom']
cls.warehouse = cls.env.ref('stock.warehouse0')
route_manufacture = cls.warehouse.manufacture_pull_id.route_id.id
route_mto = cls.warehouse.mto_pull_id.route_id.id
cls.uom_unit_id = cls.env.ref('uom.product_uom_unit').id
def create_product(name, route_ids=[]):
return cls.env['product.product'].create({
'name': name,
'type': 'product',
'route_ids': route_ids})
# Create product A, B, C.
# --------------------------
cls.product_a = create_product('Product A', route_ids=[(6, 0, [route_manufacture, route_mto])])
cls.product_b = create_product('Product B', route_ids=[(6, 0, [route_manufacture, route_mto])])
cls.product_c_id = create_product('Product C', route_ids=[]).id
cls.bom_byproduct = cls.MrpBom.create({
'product_tmpl_id': cls.product_a.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'product_uom_id': cls.uom_unit_id,
'bom_line_ids': [(0, 0, {'product_id': cls.product_c_id, 'product_uom_id': cls.uom_unit_id, 'product_qty': 2})],
'byproduct_ids': [(0, 0, {'product_id': cls.product_b.id, 'product_uom_id': cls.uom_unit_id, 'product_qty': 1})]
})
def test_00_mrp_byproduct(self):
""" Test by product with production order."""
# Create BOM for product B
# ------------------------
bom_product_b = self.MrpBom.create({
'product_tmpl_id': self.product_b.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'product_uom_id': self.uom_unit_id,
'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})]
})
# Create production order for product A
# -------------------------------------
mnf_product_a_form = Form(self.env['mrp.production'])
mnf_product_a_form.product_id = self.product_a
mnf_product_a_form.bom_id = self.bom_byproduct
mnf_product_a_form.product_qty = 2.0
mnf_product_a = mnf_product_a_form.save()
mnf_product_a.action_confirm()
# I confirm the production order.
self.assertEqual(mnf_product_a.state, 'confirmed', 'Production order should be in state confirmed')
# Now I check the stock moves for the byproduct I created in the bill of material.
# This move is created automatically when I confirmed the production order.
moves = mnf_product_a.move_raw_ids | mnf_product_a.move_finished_ids
self.assertTrue(moves, 'No moves are created !')
# I consume and produce the production of products.
# I create record for selecting mode and quantity of products to produce.
mo_form = Form(mnf_product_a)
mo_form.qty_producing = 2.00
mnf_product_a = mo_form.save()
# I finish the production order.
self.assertEqual(len(mnf_product_a.move_raw_ids), 1, "Wrong consume move on production order.")
consume_move_c = mnf_product_a.move_raw_ids
by_product_move = mnf_product_a.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_b.id)
# Check sub production produced quantity...
self.assertEqual(consume_move_c.product_uom_qty, 4, "Wrong consumed quantity of product c.")
self.assertEqual(by_product_move.product_uom_qty, 2, "Wrong produced quantity of sub product.")
mnf_product_a._post_inventory()
# I see that stock moves of External Hard Disk including Headset USB are done now.
self.assertFalse(any(move.state != 'done' for move in moves), 'Moves are not done!')
def test_01_mrp_byproduct(self):
self.env["stock.quant"].create({
"product_id": self.product_c_id,
"location_id": self.warehouse.lot_stock_id.id,
"quantity": 4,
})
bom_product_a = self.MrpBom.create({
'product_tmpl_id': self.product_a.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'product_uom_id': self.uom_unit_id,
'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})]
})
mnf_product_a_form = Form(self.env['mrp.production'])
mnf_product_a_form.product_id = self.product_a
mnf_product_a_form.bom_id = bom_product_a
mnf_product_a_form.product_qty = 2.0
mnf_product_a = mnf_product_a_form.save()
mnf_product_a.action_confirm()
self.assertEqual(mnf_product_a.state, "confirmed")
mnf_product_a.move_raw_ids._action_assign()
mnf_product_a.move_raw_ids.quantity_done = mnf_product_a.move_raw_ids.product_uom_qty
mnf_product_a.move_raw_ids._action_done()
self.assertEqual(mnf_product_a.state, "progress")
mnf_product_a.qty_producing = 2
mnf_product_a.button_mark_done()
self.assertTrue(mnf_product_a.move_finished_ids)
self.assertEqual(mnf_product_a.state, "done")
def test_change_product(self):
""" Create a production order for a specific product with a BoM. Then change the BoM and the finished product for
other ones and check the finished product of the first mo did not became a byproduct of the second one."""
# Create BOM for product A with product B as component
bom_product_a = self.MrpBom.create({
'product_tmpl_id': self.product_a.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'product_uom_id': self.uom_unit_id,
'bom_line_ids': [(0, 0, {'product_id': self.product_b.id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})],
})
bom_product_a_2 = self.MrpBom.create({
'product_tmpl_id': self.product_b.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'product_uom_id': self.uom_unit_id,
'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})],
})
# Create production order for product A
# -------------------------------------
mnf_product_a_form = Form(self.env['mrp.production'])
mnf_product_a_form.product_id = self.product_a
mnf_product_a_form.bom_id = bom_product_a
mnf_product_a_form.product_qty = 1.0
mnf_product_a = mnf_product_a_form.save()
mnf_product_a_form = Form(mnf_product_a)
mnf_product_a_form.bom_id = bom_product_a_2
mnf_product_a = mnf_product_a_form.save()
self.assertEqual(mnf_product_a.move_raw_ids.product_id.id, self.product_c_id)
self.assertFalse(mnf_product_a.move_byproduct_ids)
def test_default_uom(self):
""" Tests the `uom_id` on the byproduct gets set automatically while creating a byproduct with a product,
without the need to call an onchange or to set the uom manually in the create.
"""
# Set a specific UOM on the byproduct on purpose to make sure it's not just a default on the unit UOM
# that makes the test pass.
self.product_b.product_tmpl_id.uom_id = self.env.ref('uom.product_uom_dozen')
bom = self.MrpBom.create({
'product_tmpl_id': self.product_a.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'byproduct_ids': [(0, 0, {'product_id': self.product_b.id, 'product_qty': 1})]
})
self.assertEqual(bom.byproduct_ids.product_uom_id, self.env.ref('uom.product_uom_dozen'))
def test_finished_and_byproduct_moves(self):
"""
Tests the behavior of the `create` override in the model `mrp.production`
regarding the values for the fields `move_finished_ids` and `move_byproduct_ids`.
The behavior is a bit tricky, because the moves included in `move_byproduct_ids`
are included in the `move_finished_ids`. `move_byproduct_ids` is a subset of `move_finished_ids`.
So, when creating a manufacturing order, whether:
- Only `move_finished_ids` is passed, containing both the finished product and the by-products of the BOM,
- Only `move_byproduct_ids` is passed, only containing the by-products of the BOM,
- Both `move_finished_ids` and `move_byproduct_ids` are passed,
holding the product finished and the byproducts respectively
At the end, in the created manufacturing order
`move_finished_ids` must contain both the finished product, and the by-products,
`move_byproduct_ids` must contain only the by-products.
Besides, the code shouldn't raise an error
because only one of the two `move_finished_ids`, `move_byproduct_ids` is provided.
In addition, the test voluntary sets a different produced quantity
for the finished product and the by-products moves than defined in the BOM
as it's the point to manually pass the `move_finished_ids` and `move_byproduct_ids`
when creating a manufacturing order, set different values than the defaults, in this case
a different produced quantity than the defaults from the BOM.
"""
bom_product_a = self.MrpBom.create({
'product_tmpl_id': self.product_a.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [(0, 0, {
'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2.0
})],
'byproduct_ids': [(0, 0, {
'product_id': self.product_b.id, 'product_uom_id': self.uom_unit_id, 'product_qty': 1.0
})]
})
for expected_finished_qty, expected_byproduct_qty, values in [
# Only `move_finished_ids` passed, containing both the finished product and the by-product
(3.0, 4.0, {
'move_finished_ids': [
(0, 0, {
'product_id': self.product_a.id,
'product_uom_qty': 3.0,
'location_id': self.product_a.property_stock_production,
'location_dest_id': self.warehouse.lot_stock_id.id,
}),
(0, 0, {
'product_id': self.product_b.id,
'product_uom_qty': 4.0,
'location_id': self.product_a.property_stock_production,
'location_dest_id': self.warehouse.lot_stock_id.id,
}),
],
}),
# Only `move_byproduct_ids` passed, containing the by-product move only
(2.0, 4.0, {
'move_byproduct_ids': [
(0, 0, {
'product_id': self.product_b.id,
'product_uom_qty': 4.0,
'location_id': self.product_a.property_stock_production,
'location_dest_id': self.warehouse.lot_stock_id.id,
}),
],
}),
# Both `move_finished_ids` and `move_byproduct_ids` passed,
# containing respectively the finished product and the by-product
(3.0, 4.0, {
'move_finished_ids': [
(0, 0, {
'product_id': self.product_a.id,
'product_uom_qty': 3.0,
'location_id': self.product_a.property_stock_production,
'location_dest_id': self.warehouse.lot_stock_id.id,
}),
],
'move_byproduct_ids': [
(0, 0, {
'product_id': self.product_b.id,
'product_uom_qty': 4.0,
'location_id': self.product_a.property_stock_production,
'location_dest_id': self.warehouse.lot_stock_id.id,
}),
],
}),
]:
mo = self.env['mrp.production'].create({
'product_id': self.product_a.id,
'bom_id': bom_product_a.id,
'product_qty': 2.0,
**values,
})
self.assertEqual(mo.move_finished_ids.product_id, self.product_a + self.product_b)
self.assertEqual(mo.move_byproduct_ids.product_id, self.product_b)
finished_move = mo.move_finished_ids.filtered(lambda x: x.product_id == self.product_a)
self.assertEqual(
finished_move.product_uom_qty, expected_finished_qty, "Wrong produced quantity of finished product."
)
by_product_move = mo.move_finished_ids.filtered(lambda x: x.product_id == self.product_b)
self.assertEqual(
by_product_move.product_uom_qty, expected_byproduct_qty, "Wrong produced quantity of by-product."
)
# Also check the produced quantity of the by-product through `move_byproduct_ids`
self.assertEqual(
mo.move_byproduct_ids.product_uom_qty, expected_byproduct_qty, "Wrong produced quantity of by-product."
)
def test_byproduct_putaway(self):
"""
Test the byproducts are dispatched correctly with putaway rules. We have
a byproduct P and two sublocations L01, L02 with a capacity constraint:
max 2 x P by location. There is already 1 x P at L01. Process a MO with
2 x P as byproducts. They should be redirected to L02
"""
self.stock_location = self.env.ref('stock.stock_location_stock')
stor_category = self.env['stock.storage.category'].create({
'name': 'Super Storage Category',
'max_weight': 1000,
'product_capacity_ids': [(0, 0, {
'product_id': self.product_b.id,
'quantity': 2,
})]
})
shelf1_location = self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': stor_category.id,
})
shelf2_location = self.env['stock.location'].create({
'name': 'shelf2',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': stor_category.id,
})
self.env['stock.putaway.rule'].create({
'product_id': self.product_b.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': stor_category.id,
})
self.env['stock.putaway.rule'].create({
'product_id': self.product_a.id,
'location_in_id': self.stock_location.id,
'location_out_id': shelf2_location.id,
})
self.env['stock.quant']._update_available_quantity(self.product_b, shelf1_location, 1)
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_a
mo_form.bom_id = self.bom_byproduct
mo_form.product_qty = 2.0
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 2.00
mo = mo_form.save()
mo._post_inventory()
byproduct_move_line = mo.move_byproduct_ids.move_line_ids
finished_move_line = mo.move_finished_ids.filtered(lambda m: m.product_id == self.product_a).move_line_ids
self.assertEqual(byproduct_move_line.location_dest_id, shelf2_location)
self.assertEqual(finished_move_line.location_dest_id, shelf2_location)
def test_check_byproducts_cost_share(self):
"""
Test that byproducts with total cost_share > 100% or a cost_share < 0%
will throw a ValidationError
"""
# Create new MO
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_a
mo_form.product_qty = 2.0
mo = mo_form.save()
# Create product
self.product_d = self.env['product.product'].create({
'name': 'Product D',
'type': 'product'})
self.product_e = self.env['product.product'].create({
'name': 'Product E',
'type': 'product'})
# Create byproduct
byproduct_1 = self.env['stock.move'].create({
'name': 'By Product 1',
'product_id': self.product_d.id,
'product_uom': self.ref('uom.product_uom_unit'),
'production_id': mo.id,
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.ref('stock.stock_location_output'),
'product_uom_qty': 0,
'quantity_done': 0
})
byproduct_2 = self.env['stock.move'].create({
'name': 'By Product 2',
'product_id': self.product_e.id,
'product_uom': self.ref('uom.product_uom_unit'),
'production_id': mo.id,
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.ref('stock.stock_location_output'),
'product_uom_qty': 0,
'quantity_done': 0
})
# Update byproduct has cost share > 100%
with self.assertRaises(ValidationError), self.cr.savepoint():
byproduct_1.cost_share = 120
mo.write({'move_byproduct_ids': [(4, byproduct_1.id)]})
# Update byproduct has cost share < 0%
with self.assertRaises(ValidationError), self.cr.savepoint():
byproduct_1.cost_share = -10
mo.write({'move_byproduct_ids': [(4, byproduct_1.id)]})
# Update byproducts have total cost share > 100%
with self.assertRaises(ValidationError), self.cr.savepoint():
byproduct_1.cost_share = 60
byproduct_2.cost_share = 70
mo.write({'move_byproduct_ids': [(6, 0, [byproduct_1.id, byproduct_2.id])]})
def test_check_byproducts_cost_share_02(self):
"""
Test that byproducts with total cost_share < 100% with a cancelled moves will don't throw a ValidationError
"""
self.bom_byproduct.byproduct_ids[0].cost_share = 70
self.bom_byproduct.byproduct_ids[0].product_qty = 2
mo = self.env["mrp.production"].create({
'product_id': self.product_a.id,
'product_qty': 1.0,
'bom_id': self.bom_byproduct.id,
})
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
self.assertEqual(mo.state, 'to_close')
mo.move_byproduct_ids[0].quantity_done = 1
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
def test_3_steps_byproduct(self):
""" Test that non-bom byproducts are correctly pushed from
post-production to the stock location in 3-steps manufacture. """
self.warehouse.manufacture_steps = 'pbm_sam'
self.env.user.groups_id += self.env.ref('mrp.group_mrp_byproducts')
component, final_product, byproduct = self.env['product.product'].create([{
'name': name,
'type': 'product'
} for name in ['Old Blood', 'Insight', 'Eyes on the Inside']])
self.env['stock.quant']._update_available_quantity(component, self.warehouse.lot_stock_id, 1)
mo = self.env["mrp.production"].create({
'product_id': final_product.id,
'product_qty': 1.0,
})
mo_form = Form(mo)
with mo_form.move_raw_ids.new() as line:
line.product_id = component
with mo_form.move_byproduct_ids.new() as line:
line.product_id = byproduct
mo = mo_form.save()
mo.action_confirm()
preprod_picking = mo.picking_ids.filtered(lambda p: p.state == 'assigned')
preprod_picking.move_line_ids.qty_done = 1
preprod_picking.button_validate()
mo.move_raw_ids.quantity_done = 1
mo.qty_producing = 1
mo.button_mark_done()
postprod_picking = mo.picking_ids.filtered(lambda p: p.state == 'assigned')
self.assertEqual(len(postprod_picking.move_ids), 2)
self.assertEqual(postprod_picking.move_ids.product_id, final_product + byproduct)
self.assertEqual(postprod_picking.location_dest_id, self.warehouse.lot_stock_id)

View file

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from datetime import datetime, timedelta
from odoo.fields import Datetime as Dt
from odoo.exceptions import UserError
from odoo.addons.mrp.tests.common import TestMrpCommon
class TestMrpCancelMO(TestMrpCommon):
def test_cancel_mo_without_routing_1(self):
""" Cancel a Manufacturing Order with no routing, no production.
"""
# Create MO
manufacturing_order = self.generate_mo()[0]
# Do nothing, cancel it
manufacturing_order.action_cancel()
# Check the MO and its moves are cancelled
self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.")
self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'cancel',
"Cancelled MO raw moves must be cancelled as well.")
self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'cancel',
"Cancelled MO raw moves must be cancelled as well.")
self.assertEqual(manufacturing_order.move_finished_ids.state, 'cancel',
"Cancelled MO finished move must be cancelled as well.")
def test_cancel_mo_without_routing_2(self):
""" Cancel a Manufacturing Order with no routing but some productions.
"""
# Create MO
manufacturing_order = self.generate_mo()[0]
# Produce some quantity
mo_form = Form(manufacturing_order)
mo_form.qty_producing = 2
manufacturing_order = mo_form.save()
# Cancel it
manufacturing_order.action_cancel()
# Check it's cancelled
self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.")
self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'cancel',
"Cancelled MO raw moves must be cancelled as well.")
self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'cancel',
"Cancelled MO raw moves must be cancelled as well.")
self.assertEqual(manufacturing_order.move_finished_ids.state, 'cancel',
"Cancelled MO finished move must be cancelled as well.")
def test_cancel_mo_without_routing_3(self):
""" Cancel a Manufacturing Order with no routing but some productions
after post inventory.
"""
# Create MO
manufacturing_order = self.generate_mo(consumption='strict')[0]
# Produce some quantity (not all to avoid to done the MO when post inventory)
mo_form = Form(manufacturing_order)
mo_form.qty_producing = 2
manufacturing_order = mo_form.save()
# Post Inventory
manufacturing_order._post_inventory()
# Cancel the MO
manufacturing_order.action_cancel()
# Check MO is marked as done and its SML are done or cancelled
self.assertEqual(manufacturing_order.state, 'done', "MO should be in done state.")
self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'done',
"Due to 'post_inventory', some move raw must stay in done state")
self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'done',
"Due to 'post_inventory', some move raw must stay in done state")
self.assertEqual(manufacturing_order.move_raw_ids[2].state, 'cancel',
"The other move raw are cancelled like their MO.")
self.assertEqual(manufacturing_order.move_raw_ids[3].state, 'cancel',
"The other move raw are cancelled like their MO.")
self.assertEqual(manufacturing_order.move_finished_ids[0].state, 'done',
"Due to 'post_inventory', a move finished must stay in done state")
self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
"The other move finished is cancelled like its MO.")
def test_unlink_mo(self):
""" Try to unlink a Manufacturing Order, and check it's possible or not
depending of the MO state (must be in cancel state to be unlinked, but
the unlink method will try to cancel MO before unlink them).
"""
# Case #1: Create MO, do nothing and try to unlink it (can be deleted)
manufacturing_order = self.generate_mo()[0]
self.assertEqual(manufacturing_order.exists().state, 'confirmed')
manufacturing_order.unlink()
# Check the MO is deleted.
self.assertEqual(manufacturing_order.exists().state, False)
# Case #2: Create MO, make and post some production, then try to unlink
# it (cannot be deleted)
manufacturing_order = self.generate_mo()[0]
# Produce some quantity (not all to avoid to done the MO when post inventory)
mo_form = Form(manufacturing_order)
mo_form.qty_producing = 2
manufacturing_order = mo_form.save()
# Post Inventory
manufacturing_order._post_inventory()
# Unlink the MO must raises an UserError since it cannot be really cancelled
self.assertEqual(manufacturing_order.exists().state, 'progress')
with self.assertRaises(UserError):
manufacturing_order.unlink()
def test_cancel_mo_without_component(self):
product_form = Form(self.env['product.product'])
product_form.name = "SuperProduct"
product = product_form.save()
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product
mo = mo_form.save()
mo.action_confirm()
mo.action_cancel()
self.assertEqual(mo.move_finished_ids.state, 'cancel')
self.assertEqual(mo.state, 'cancel')

View file

@ -0,0 +1,157 @@
from odoo.tests import tagged
from odoo.addons.mrp.tests.common_consume_tracked_component import TestConsumeTrackedComponentCommon
@tagged('post_install', '-at_install')
class TestConsumeTrackedComponent(TestConsumeTrackedComponentCommon):
def test_option_disabled_and_qty_available(self):
"""
Option disabled, qty available
-> Not Tracked components are fully consumed
-> Tracked components are only consumed on button_mark_done trigger
"""
self.picking_type.use_auto_consume_components_lots = False
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT)
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
mo_all = mo_none + mo_serial + mo_lot
mo_all.action_confirm()
all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT
quant = self.create_quant(self.raw_none, 3*all_qty)
quant |= self.create_quant(self.raw_lot, 2*all_qty)
quant |= self.create_quant(self.raw_serial, 1*all_qty)
quant.action_apply_inventory()
#Quantities are fully reserved (stock.move state is available)
mo_all.action_assign()
for mov in mo_all.move_raw_ids:
self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.")
#Test for Serial Product
self.executeConsumptionTriggers(mo_serial)
self.executeConsumptionTriggers(mo_none)
self.executeConsumptionTriggers(mo_lot)
for mov in mo_all.move_raw_ids:
if mov.has_tracking == 'none' or mov.raw_material_production_id.state == 'done':
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
else:
self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to 0.")
def test_option_enabled_and_qty_available(self):
"""
Option enabled, qty available
-> Not Tracked components are fully consumed
-> Tracked components are fully consumed
"""
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT)
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
mo_all = mo_none + mo_serial + mo_lot
mo_all.action_confirm()
all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT
quant = self.create_quant(self.raw_none, 3*all_qty)
quant |= self.create_quant(self.raw_lot, 2*all_qty)
quant |= self.create_quant(self.raw_serial, 1*all_qty)
quant.action_apply_inventory()
#Quantities are fully reserved (stock.move state is available)
mo_all.action_assign()
for mov in mo_all.move_raw_ids:
self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.")
self.executeConsumptionTriggers(mo_serial)
self.executeConsumptionTriggers(mo_none)
self.executeConsumptionTriggers(mo_lot)
for mov in mo_all.move_raw_ids:
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
def test_option_enabled_and_qty_not_available(self):
"""
Option enabled, qty not available
-> Not Tracked components are fully consumed
-> Tracked components are not consumed
"""
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_TRIGGERS_COUNT)
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_TRIGGERS_COUNT)
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_TRIGGERS_COUNT)
mo_all = mo_none + mo_serial + mo_lot
mo_all.action_confirm()
#Quantities are not reserved at all (stock.move state is confirmed)
mo_all.action_assign()
for mov in mo_all.move_raw_ids:
self.assertEqual(0, mov.reserved_availability, "Reserved quantity shall be equal to 0.")
self.executeConsumptionTriggers(mo_serial)
self.executeConsumptionTriggers(mo_none)
self.executeConsumptionTriggers(mo_lot)
for mov in mo_all.move_raw_ids:
if mov.has_tracking == 'none':
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
else:
self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
def test_option_enabled_and_qty_partially_available(self):
"""
Option enabled, qty partially available
-> Not Tracked components are fully consumed
-> Tracked components are partially consumed
"""
#update BoM serial component qty
self.bom_none_lines[2].product_qty = 2
self.bom_serial_lines[2].product_qty = 2
self.bom_lot_lines[2].product_qty = 2
raw_none_qty = 2
raw_tracked_qty = 1
quant = self.create_quant(self.raw_none, raw_none_qty)
quant |= self.create_quant(self.raw_lot, raw_tracked_qty)
quant |= self.create_quant(self.raw_serial, raw_tracked_qty)
quant.action_apply_inventory()
#We must create & process each MO at once as we must assign quants for each individually
def testUnit(mo_tmpl, serialTrigger=None):
mo = self.create_mo(mo_tmpl, 1)
mo.action_confirm()
#Quantities are partially reserved (stock.move state is partially_available)
mo.action_assign()
for mov in mo.move_raw_ids:
if mov.has_tracking == "none":
self.assertEqual(raw_none_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_none_qty)+ ".")
else:
self.assertEqual(raw_tracked_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_tracked_qty)+ ".")
if serialTrigger is None:
self.executeConsumptionTriggers(mo)
elif serialTrigger == 1:
mo.qty_producing = mo.product_qty
mo._onchange_producing()
elif serialTrigger == 2:
mo.action_generate_serial()
for mov in mo.move_raw_ids:
if mov.has_tracking == "none":
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
else:
self.assertEqual(raw_tracked_qty, mov.quantity_done, "Done quantity shall be equal to " + str(raw_tracked_qty)+ ".")
mo.action_cancel()
testUnit(self.mo_none_tmpl)
testUnit(self.mo_lot_tmpl)
testUnit(self.mo_serial_tmpl, 1)
testUnit(self.mo_serial_tmpl, 2)

View file

@ -0,0 +1,190 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# -*- coding: utf-8 -*-
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tests import tagged, Form, HttpCase
@tagged('post_install', '-at_install')
class TestTourManualConsumption(HttpCase):
def test_mrp_manual_consumption(self):
"""Test manual consumption mechanism. Test when manual consumption is
True, quantity_done won't be updated automatically. Bom line with tracked
products or operations should be set to manual consumption automatically.
Also test that when manually change quantity_done, manual consumption
will be set to True. Also test when create backorder, the manual consumption
should be set according to the bom.
"""
Product = self.env['product.product']
product_finish = Product.create({
'name': 'finish',
'type': 'product',
'tracking': 'none',})
product_nt = Product.create({
'name': 'No tracking',
'type': 'product',
'tracking': 'none',})
product_sn = Product.create({
'name': 'Serial',
'type': 'product',
'tracking': 'serial',})
product_lot = Product.create({
'name': 'Lot',
'type': 'product',
'tracking': 'lot',})
bom = self.env['mrp.bom'].create({
'product_id': product_finish.id,
'product_tmpl_id': product_finish.product_tmpl_id.id,
'product_qty': 1,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_nt.id, 'product_qty': 1}),
(0, 0, {'product_id': product_sn.id, 'product_qty': 1}),
(0, 0, {'product_id': product_lot.id, 'product_qty': 1}),
],
})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_finish
mo_form.bom_id = bom
mo_form.product_qty = 10
mo = mo_form.save()
mo.action_confirm()
mo.action_assign()
# test no updating
mo_form = Form(mo)
mo_form.qty_producing = 5
mo = mo_form.save()
move_nt, move_sn, move_lot = mo.move_raw_ids
self.assertEqual(move_nt.manual_consumption, False)
self.assertEqual(move_nt.quantity_done, 5)
self.assertEqual(move_sn.manual_consumption, True)
self.assertEqual(move_sn.quantity_done, 0)
self.assertEqual(move_lot.manual_consumption, True)
self.assertEqual(move_lot.quantity_done, 0)
action_id = self.env.ref('mrp.menu_mrp_production_action').action
url = "/web#model=mrp.production&view_type=form&action=%s&id=%s" % (str(action_id.id), str(mo.id))
self.start_tour(url, "test_mrp_manual_consumption", login="admin", timeout=200)
self.assertEqual(move_nt.manual_consumption, True)
self.assertEqual(move_nt.quantity_done, 6.0)
self.assertEqual(move_sn.manual_consumption, True)
self.assertEqual(move_sn.quantity_done, 0)
self.assertEqual(move_lot.manual_consumption, True)
self.assertEqual(move_lot.quantity_done, 0)
backorder = mo.procurement_group_id.mrp_production_ids - mo
move_nt = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_nt)
move_sn = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_sn)
move_lot = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_lot)
self.assertEqual(move_nt.manual_consumption, False)
self.assertEqual(move_sn.manual_consumption, True)
self.assertEqual(move_lot.manual_consumption, True)
class TestManualConsumption(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)]})
def test_manual_consumption_backorder_00(self):
"""Test when use_auto_consume_components_lots is not set, manual consumption
of the backorder is correctly set.
"""
mo, _, _final, c1, c2 = self.generate_mo('none', 'lot', 'none', qty_final=2)
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
lot = self.env['stock.lot'].create({
'name': 'lot',
'product_id': c1.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(c1, self.stock_location, 8, lot_id=lot)
self.env['stock.quant']._update_available_quantity(c2, self.stock_location, 2)
mo.action_assign()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo_form.save()
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c1.id).mapped("quantity_done")), 0)
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c2.id).mapped("quantity_done")), 1)
details_operation_form = Form(mo.move_raw_ids.filtered(lambda m: m.product_id == c1), view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 4
ml.lot_id = lot
details_operation_form.save()
action = mo.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
backorder_mo = mo.procurement_group_id.mrp_production_ids[-1]
self.assertTrue(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
def test_manual_consumption_backorder_01(self):
"""Test when use_auto_consume_components_lots is set, manual consumption
of the backorder is correctly set.
"""
picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
picking_type.use_auto_consume_components_lots = True
mo, _, _final, c1, c2 = self.generate_mo('none', 'lot', 'none', qty_final=2)
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
lot = self.env['stock.lot'].create({
'name': 'lot',
'product_id': c1.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(c1, self.stock_location, 8, lot_id=lot)
self.env['stock.quant']._update_available_quantity(c2, self.stock_location, 2)
mo.action_assign()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo_form.save()
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c1.id).mapped("quantity_done")), 4)
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c2.id).mapped("quantity_done")), 1)
action = mo.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
backorder_mo = mo.procurement_group_id.mrp_production_ids[-1]
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
def test_manual_consumption_split_merge_00(self):
"""Test manual consumption is correctly set after split or merge.
"""
# Change 'Units' rounding to 1 (integer only quantities)
self.uom_unit.rounding = 1
# Create a mo for 10 products
mo, _, _, p1, p2 = self.generate_mo('none', 'lot', 'none', qty_final=10)
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)
# Split in 3 parts
action = mo.action_split()
wizard = Form(self.env[action['res_model']].with_context(action['context']))
wizard.counter = 3
action = wizard.save().action_split()
for production in mo.procurement_group_id.mrp_production_ids:
self.assertTrue(production.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
self.assertFalse(production.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)
# Merge them back
action = mo.procurement_group_id.mrp_production_ids.action_merge()
mo = self.env[action['res_model']].browse(action['res_id'])
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)

View file

@ -0,0 +1,284 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common, Form
from odoo.exceptions import UserError
class TestMrpMulticompany(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
group_user = cls.env.ref('base.group_user')
group_mrp_manager = cls.env.ref('mrp.group_mrp_manager')
cls.company_a = cls.env['res.company'].create({'name': 'Company A'})
cls.company_b = cls.env['res.company'].create({'name': 'Company B'})
cls.warehouse_a = cls.env['stock.warehouse'].search([('company_id', '=', cls.company_a.id)], limit=1)
cls.warehouse_b = cls.env['stock.warehouse'].search([('company_id', '=', cls.company_b.id)], limit=1)
cls.stock_location_a = cls.warehouse_a.lot_stock_id
cls.stock_location_b = cls.warehouse_b.lot_stock_id
cls.user_a = cls.env['res.users'].create({
'name': 'user company a with access to company b',
'login': 'user a',
'groups_id': [(6, 0, [group_user.id, group_mrp_manager.id])],
'company_id': cls.company_a.id,
'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
})
cls.user_b = cls.env['res.users'].create({
'name': 'user company a with access to company b',
'login': 'user b',
'groups_id': [(6, 0, [group_user.id, group_mrp_manager.id])],
'company_id': cls.company_b.id,
'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
})
def test_bom_1(self):
"""Check it is not possible to use a product of Company B in a
bom of Company A. """
product_b = self.env['product.product'].create({
'name': 'p1',
'company_id': self.company_b.id,
})
with self.assertRaises(UserError):
self.env['mrp.bom'].create({
'product_id': product_b.id,
'product_tmpl_id': product_b.product_tmpl_id.id,
'company_id': self.company_a.id,
})
def test_bom_2(self):
"""Check it is not possible to use a product of Company B as a component
in a bom of Company A. """
product_a = self.env['product.product'].create({
'name': 'p1',
'company_id': self.company_a.id,
})
product_b = self.env['product.product'].create({
'name': 'p2',
'company_id': self.company_b.id,
})
with self.assertRaises(UserError):
self.env['mrp.bom'].create({
'product_id': product_a.id,
'product_tmpl_id': product_b.product_tmpl_id.id,
'company_id': self.company_a.id,
'bom_line_ids': [(0, 0, {'product_id': product_b.id})]
})
def test_production_1(self):
"""Check it is not possible to confirm a production of Company B with
product of Company A. """
product_a = self.env['product.product'].create({
'name': 'p1',
'company_id': self.company_a.id,
})
mo = self.env['mrp.production'].create({
'product_id': product_a.id,
'product_uom_id': product_a.uom_id.id,
'company_id': self.company_b.id,
})
with self.assertRaises(UserError):
mo.action_confirm()
def test_production_2(self):
"""Check that confirming a production in company b with user_a will create
stock moves on company b. """
product_a = self.env['product.product'].create({
'name': 'p1',
'company_id': self.company_a.id,
})
component_a = self.env['product.product'].create({
'name': 'p2',
'company_id': self.company_a.id,
})
self.env['mrp.bom'].create({
'product_id': product_a.id,
'product_tmpl_id': product_a.product_tmpl_id.id,
'company_id': self.company_a.id,
'bom_line_ids': [(0, 0, {'product_id': component_a.id})]
})
mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
mo_form.product_id = product_a
mo = mo_form.save()
mo.with_user(self.user_b).action_confirm()
self.assertEqual(mo.move_raw_ids.company_id, self.company_a)
self.assertEqual(mo.move_finished_ids.company_id, self.company_a)
def test_product_produce_1(self):
"""Check that using a finished lot of company b in the produce wizard of a production
of company a is not allowed """
product = self.env['product.product'].create({
'name': 'p1',
'tracking': 'lot',
})
component = self.env['product.product'].create({
'name': 'p2',
})
lot_b = self.env['stock.lot'].create({
'product_id': product.id,
'company_id': self.company_b.id,
})
self.env['mrp.bom'].create({
'product_id': product.id,
'product_tmpl_id': product.product_tmpl_id.id,
'company_id': self.company_a.id,
'bom_line_ids': [(0, 0, {'product_id': component.id})]
})
mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
mo_form.product_id = product
# The mo must be confirmed, no longer in draft, in order for `lot_producing_id` to be visible in the view
# <div class="o_row" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}">
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.lot_producing_id = lot_b
mo = mo_form.save()
with self.assertRaises(UserError):
mo.with_user(self.user_b).action_confirm()
def test_product_produce_2(self):
"""Check that using a component lot of company b in the produce wizard of a production
of company a is not allowed """
product = self.env['product.product'].create({
'name': 'p1',
})
component = self.env['product.product'].create({
'name': 'p2',
'tracking': 'lot',
})
lot_b = self.env['stock.lot'].create({
'product_id': component.id,
'company_id': self.company_b.id,
})
self.env['mrp.bom'].create({
'product_id': product.id,
'product_tmpl_id': product.product_tmpl_id.id,
'company_id': self.company_a.id,
'bom_line_ids': [(0, 0, {'product_id': component.id})]
})
mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
mo_form.product_id = product
mo = mo_form.save()
mo.with_user(self.user_b).action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 1
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.lot_id = lot_b
ml.qty_done = 1
details_operation_form.save()
with self.assertRaises(UserError):
mo.button_mark_done()
def test_is_kit_in_multi_company_env(self):
""" Check that is_kits is company dependant """
product1, product2 = self.env['product.product'].create([{'name': 'Kit Kat'}, {'name': 'twix'}])
self.env['mrp.bom'].create([{
'product_id': product1.id,
'product_tmpl_id': product1.product_tmpl_id.id,
'company_id': self.company_a.id,
'type': 'phantom',
}, {
'product_id': product2.id,
'product_tmpl_id': product2.product_tmpl_id.id,
'company_id': False,
'type': 'phantom',
}])
template1 = product1.product_tmpl_id
template2 = product2.product_tmpl_id
self.assertFalse(product1.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
self.assertFalse(template1.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
self.assertTrue(product1.with_company(self.company_a).is_kits)
self.assertTrue(template1.with_company(self.company_a).is_kits)
self.assertFalse(product1.with_company(self.company_b).is_kits)
self.assertFalse(template1.with_company(self.company_b).is_kits)
self.assertTrue(product2.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
self.assertTrue(template2.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
self.assertTrue(product2.with_company(self.company_a).is_kits)
self.assertTrue(template2.with_company(self.company_a).is_kits)
self.assertTrue(product2.with_company(self.company_b).is_kits)
self.assertTrue(template2.with_company(self.company_b).is_kits)
def test_partner_1(self):
""" On a product without company, as a user of Company B, check it is not possible to use a
location limited to Company A as `property_stock_production` """
shared_product = self.env['product.product'].create({
'name': 'Shared Product',
'company_id': False,
})
with self.assertRaises(UserError):
shared_product.with_user(self.user_b).property_stock_production = self.stock_location_a
def test_company_specific_routes_and_company_creation(self):
"""
Setup: company-specific manufacture routes
Use case: create a new company
A manufacture route should be created for the new company
"""
company = self.env.company
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1)
manufacture_rule = warehouse.manufacture_pull_id
manufacture_route = manufacture_rule.route_id
# Allocate each company-specific manufacture rule to a new route
for rule in manufacture_route.rule_ids.sudo():
rule_company = rule.company_id
if not rule_company or rule_company == company:
continue
manufacture_route.copy({
'company_id': rule_company.id,
'rule_ids': [(4, rule.id)],
})
# Also specify the company of the "generic route" (the one from the master data)
manufacture_route.company_id = company
new_company = self.env['res.company'].create({'name': 'Super Company'})
new_warehouse = self.env['stock.warehouse'].search([('company_id', '=', new_company.id)], limit=1)
self.assertEqual(new_warehouse.manufacture_pull_id.route_id.company_id, new_company)
def test_company_specific_routes_and_warehouse_creation(self):
""" Check that we are able to create a new warehouse when the generic manufacture route
is in a different company. """
group_stock_manager = self.env.ref('stock.group_stock_manager')
self.user_a.write({'groups_id': [(4, group_stock_manager.id)]})
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture')
for rule in manufacture_route.rule_ids.sudo():
rule_company = rule.company_id
if not rule_company or rule_company == self.company_a:
continue
manufacture_route.copy({
'company_id': rule_company.id,
'rule_ids': [(4, rule.id)],
})
manufacture_route.company_id = self.company_a
# Enable multi warehouse
group_user = self.env.ref('base.group_user')
group_stock_multi_warehouses = self.env.ref('stock.group_stock_multi_warehouses')
group_stock_multi_locations = self.env.ref('stock.group_stock_multi_locations')
self.env['res.config.settings'].create({
'group_stock_multi_locations': True,
}).execute()
group_user.write({'implied_ids': [(4, group_stock_multi_warehouses.id), (4, group_stock_multi_locations.id)]})
new_warehouse = self.env['stock.warehouse'].with_user(self.user_a).with_context(allowed_company_ids=[self.company_b.id]).create({
'name': 'Warehouse #2',
'code': 'WH2',
})
self.assertEqual(new_warehouse.manufacture_pull_id.route_id.company_id, self.company_b)

View file

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta, time
from pytz import timezone, utc
from odoo import fields
from odoo.addons.mrp.tests.common import TestMrpCommon
class TestOee(TestMrpCommon):
def create_productivity_line(self, loss_reason, date_start=False, date_end=False):
return self.env['mrp.workcenter.productivity'].create({
'workcenter_id': self.workcenter_1.id,
'date_start': date_start,
'date_end': date_end,
'loss_id': loss_reason.id,
'description': loss_reason.name
})
def test_wrokcenter_oee(self):
""" Test case workcenter oee. """
day = datetime.date(datetime.today())
# Make the test work the weekend. It will fails due to workcenter working hours.
if day.weekday() in (5, 6):
day -= timedelta(days=2)
tz = timezone(self.workcenter_1.resource_calendar_id.tz)
def time_to_string_utc_datetime(time):
return fields.Datetime.to_string(
tz.localize(datetime.combine(day, time)).astimezone(utc)
)
start_time = time_to_string_utc_datetime(time(10, 43, 22))
end_time = time_to_string_utc_datetime(time(10, 56, 22))
# Productive time duration (13 min)
self.create_productivity_line(self.env.ref('mrp.block_reason7'), start_time, end_time)
# Material Availability time duration (1.52 min)
# Check working state is blocked or not.
start_time = time_to_string_utc_datetime(time(10, 47, 8))
workcenter_productivity_1 = self.create_productivity_line(self.env.ref('mrp.block_reason0'), start_time)
self.assertEqual(self.workcenter_1.working_state, 'blocked', "Wrong working state of workcenter.")
# Check working state is normal or not.
end_time = time_to_string_utc_datetime(time(10, 48, 39))
workcenter_productivity_1.write({'date_end': end_time})
self.assertEqual(self.workcenter_1.working_state, 'normal', "Wrong working state of workcenter.")
# Process Defect time duration (1.33 min)
start_time = time_to_string_utc_datetime(time(10, 48, 38))
end_time = time_to_string_utc_datetime(time(10, 49, 58))
self.create_productivity_line(self.env.ref('mrp.block_reason5'), start_time, end_time)
# Reduced Speed time duration (3.0 min)
start_time = time_to_string_utc_datetime(time(10, 50, 22))
end_time = time_to_string_utc_datetime(time(10, 53, 22))
self.create_productivity_line(self.env.ref('mrp.block_reason4'), start_time, end_time)
# Block time : ( Process Defact (1.33 min) + Reduced Speed (3.0 min) + Material Availability (1.52 min)) = 5.85 min
blocked_time_in_hour = round(((1.33 + 3.0 + 1.52) / 60.0), 2)
# Productive time : Productive time duration (13 min)
productive_time_in_hour = round((13.0 / 60.0), 2)
# Check blocked time and productive time
self.assertEqual(self.workcenter_1.blocked_time, blocked_time_in_hour, "Wrong block time on workcenter.")
self.assertEqual(self.workcenter_1.productive_time, productive_time_in_hour, "Wrong productive time on workcenter.")
# Check overall equipment effectiveness
computed_oee = round(((productive_time_in_hour * 100.0)/(productive_time_in_hour + blocked_time_in_hour)), 2)
self.assertEqual(self.workcenter_1.oee, computed_oee, "Wrong oee on workcenter.")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import unittest
import time
import logging
from odoo.tests import common, Form
_logger = logging.getLogger(__name__)
class TestMrpSerialMassProducePerformance(common.TransactionCase):
@unittest.skip
def test_smp_performance(self):
total_quantity = 1000
quantity = 1
raw_materials_count = 10
trackings = [
'none',
# 'lot',
# 'serial'
]
_logger.info('setting up environment')
raw_materials = []
for i in range(raw_materials_count):
raw_materials.append(self.env['product.product'].create({
'name': '@raw_material#' + str(i + 1),
'type': 'product',
'tracking': trackings[i % len(trackings)]
}))
finished = self.env['product.product'].create({
'name': '@finished',
'type': 'product',
'tracking': 'serial',
})
bom = self.env['mrp.bom'].create({
'product_id': finished.id,
'product_tmpl_id': finished.product_tmpl_id.id,
'product_uom_id': finished.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'consumption': 'flexible',
'bom_line_ids': [(0, 0, {'product_id': p[0]['id'], 'product_qty': 1}) for p in raw_materials]
})
form = Form(self.env['mrp.production'])
form.product_id = finished
form.bom_id = bom
form.product_qty = total_quantity
mo = form.save()
mo.action_confirm()
for i in range(raw_materials_count):
if raw_materials[i].tracking == 'none':
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': raw_materials[i].id,
'inventory_quantity': total_quantity,
'location_id': mo.location_src_id.id,
})._apply_inventory()
elif raw_materials[i].tracking == 'lot':
qty = total_quantity
while qty > 0:
lot = self.env['stock.lot'].create({
'product_id': raw_materials[i].id,
'company_id': self.env.company.id,
})
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': raw_materials[i].id,
'inventory_quantity': 10,
'location_id': mo.location_src_id.id,
'lot_id': lot.id,
})._apply_inventory()
qty -= 10
else:
for _ in range(total_quantity):
lot = self.env['stock.lot'].create({
'product_id': raw_materials[i].id,
'company_id': self.env.company.id,
})
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': raw_materials[i].id,
'inventory_quantity': 1,
'location_id': mo.location_src_id.id,
'lot_id': lot.id,
})._apply_inventory()
mo.action_assign()
action = mo.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
wizard.next_serial_number = "sn#1"
wizard.next_serial_count = quantity
action = wizard.save().generate_serial_numbers_production()
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard = wizard.save()
_logger.info('generating serial numbers')
start = time.perf_counter()
if quantity == total_quantity:
wizard.apply()
else:
wizard.create_backorder()
end = time.perf_counter()
_logger.info('time to produce %s/%s: %s', quantity, total_quantity, end - start)

View file

@ -0,0 +1,788 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import fields
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.exceptions import UserError
class TestProcurement(TestMrpCommon):
def test_procurement(self):
"""This test case when create production order check procurement is create"""
# Update BOM
self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
self.bom_1.bom_line_ids.filtered(lambda x: x.product_id == self.product_1).unlink()
# Update route
self.warehouse = self.env.ref('stock.warehouse0')
self.warehouse.mto_pull_id.route_id.active = True
route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
route_mto = self.warehouse.mto_pull_id.route_id.id
self.product_4.write({'route_ids': [(6, 0, [route_manufacture, route_mto])]})
# Create production order
# -------------------------
# Product6 Unit 24
# Product4 8 Dozen
# Product2 12 Unit
# -----------------------
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.product_6
production_form.bom_id = self.bom_3
production_form.product_qty = 24
production_form.product_uom_id = self.product_6.uom_id
production_product_6 = production_form.save()
production_product_6.action_confirm()
production_product_6.action_assign()
# check production state is Confirmed
self.assertEqual(production_product_6.state, 'confirmed')
# Check procurement for product 4 created or not.
# Check it created a purchase order
move_raw_product4 = production_product_6.move_raw_ids.filtered(lambda x: x.product_id == self.product_4)
produce_product_4 = self.env['mrp.production'].search([('product_id', '=', self.product_4.id),
('move_dest_ids', '=', move_raw_product4[0].id)])
# produce product
self.assertEqual(produce_product_4.reservation_state, 'confirmed', "Consume material not available")
# Create production order
# -------------------------
# Product 4 96 Unit
# Product2 48 Unit
# ---------------------
# Update Inventory
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product_2.id,
'inventory_quantity': 48,
'location_id': self.warehouse.lot_stock_id.id,
}).action_apply_inventory()
produce_product_4.action_assign()
self.assertEqual(produce_product_4.product_qty, 96, "Wrong quantity of finish product.")
self.assertEqual(produce_product_4.product_uom_id, self.uom_unit, "Wrong quantity of finish product.")
self.assertEqual(produce_product_4.reservation_state, 'assigned', "Consume material not available")
# produce product4
# ---------------
mo_form = Form(produce_product_4)
mo_form.qty_producing = produce_product_4.product_qty
produce_product_4 = mo_form.save()
# Check procurement and Production state for product 4.
produce_product_4.button_mark_done()
self.assertEqual(produce_product_4.state, 'done', 'Production order should be in state done')
# Produce product 6
# ------------------
# Update Inventory
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product_2.id,
'inventory_quantity': 12,
'location_id': self.warehouse.lot_stock_id.id,
}).action_apply_inventory()
production_product_6.action_assign()
# ------------------------------------
self.assertEqual(production_product_6.reservation_state, 'assigned', "Consume material not available")
mo_form = Form(production_product_6)
mo_form.qty_producing = production_product_6.product_qty
production_product_6 = mo_form.save()
# Check procurement and Production state for product 6.
production_product_6.button_mark_done()
self.assertEqual(production_product_6.state, 'done', 'Production order should be in state done')
self.assertEqual(self.product_6.qty_available, 24, 'Wrong quantity available of finished product.')
def test_procurement_2(self):
"""Check that a manufacturing order create the right procurements when the route are set on
a parent category of a product"""
# find a child category id
all_categ_id = self.env['product.category'].search([('parent_id', '=', None)], limit=1)
child_categ_id = self.env['product.category'].search([('parent_id', '=', all_categ_id.id)], limit=1)
# set the product of `self.bom_1` to this child category
for bom_line_id in self.bom_1.bom_line_ids:
# check that no routes are defined on the product
self.assertEqual(len(bom_line_id.product_id.route_ids), 0)
# set the category of the product to a child category
bom_line_id.product_id.categ_id = child_categ_id
# set the MTO route to the parent category (all)
self.warehouse = self.env.ref('stock.warehouse0')
mto_route = self.warehouse.mto_pull_id.route_id
mto_route.active = True
mto_route.product_categ_selectable = True
all_categ_id.write({'route_ids': [(6, 0, [mto_route.id])]})
# create MO, but check it raises error as components are in make to order and not everyone has
with self.assertRaises(UserError):
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.product_4
production_form.product_uom_id = self.product_4.uom_id
production_form.product_qty = 1
production_product_4 = production_form.save()
production_product_4.action_confirm()
def test_procurement_3(self):
warehouse = self.env['stock.warehouse'].search([], limit=1)
warehouse.write({'reception_steps': 'three_steps'})
warehouse.mto_pull_id.route_id.active = True
self.env['stock.location']._parent_store_compute()
warehouse.reception_route_id.rule_ids.filtered(
lambda p: p.location_src_id == warehouse.wh_input_stock_loc_id and
p.location_dest_id == warehouse.wh_qc_stock_loc_id).write({
'procure_method': 'make_to_stock'
})
finished_product = self.env['product.product'].create({
'name': 'Finished Product',
'type': 'product',
})
component = self.env['product.product'].create({
'name': 'Component',
'type': 'product',
'route_ids': [(4, warehouse.mto_pull_id.route_id.id)]
})
self.env['stock.quant']._update_available_quantity(component, warehouse.wh_input_stock_loc_id, 100)
bom = self.env['mrp.bom'].create({
'product_id': finished_product.id,
'product_tmpl_id': finished_product.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.0})
]})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.bom_id = bom
mo_form.product_qty = 5
mo_form.product_uom_id = finished_product.uom_id
mo_form.location_src_id = warehouse.lot_stock_id
mo = mo_form.save()
mo.action_confirm()
pickings = self.env['stock.picking'].search([('product_id', '=', component.id)])
self.assertEqual(len(pickings), 2.0)
picking_input_to_qc = pickings.filtered(lambda p: p.location_id == warehouse.wh_input_stock_loc_id)
picking_qc_to_stock = pickings - picking_input_to_qc
self.assertTrue(picking_input_to_qc)
self.assertTrue(picking_qc_to_stock)
picking_input_to_qc.action_assign()
self.assertEqual(picking_input_to_qc.state, 'assigned')
picking_input_to_qc.move_line_ids.write({'qty_done': 5.0})
picking_input_to_qc._action_done()
picking_qc_to_stock.action_assign()
self.assertEqual(picking_qc_to_stock.state, 'assigned')
picking_qc_to_stock.move_line_ids.write({'qty_done': 3.0})
picking_qc_to_stock.with_context(skip_backorder=True, picking_ids_not_to_backorder=picking_qc_to_stock.ids).button_validate()
self.assertEqual(picking_qc_to_stock.state, 'done')
mo.action_assign()
self.assertEqual(mo.move_raw_ids.reserved_availability, 3.0)
produce_form = Form(mo)
produce_form.qty_producing = 3.0
mo = produce_form.save()
self.assertEqual(mo.move_raw_ids.quantity_done, 3.0)
picking_qc_to_stock.move_line_ids.qty_done = 5.0
self.assertEqual(mo.move_raw_ids.reserved_availability, 5.0)
self.assertEqual(mo.move_raw_ids.quantity_done, 3.0)
def test_link_date_mo_moves(self):
""" Check link of shedule date for manufaturing with date stock move."""
# create a product with manufacture route
product_1 = self.env['product.product'].create({
'name': 'AAA',
'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))]
})
component_1 = self.env['product.product'].create({
'name': 'component',
})
self.env['mrp.bom'].create({
'product_id': product_1.id,
'product_tmpl_id': product_1.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_1.id, 'product_qty': 1}),
]})
# create a move for product_1 from stock to output and reserve to trigger the
# rule
move_dest = self.env['stock.move'].create({
'name': 'move_orig',
'product_id': product_1.id,
'product_uom': self.ref('uom.product_uom_unit'),
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.ref('stock.stock_location_output'),
'product_uom_qty': 10,
'procure_method': 'make_to_order'
})
move_dest._action_confirm()
mo = self.env['mrp.production'].search([
('product_id', '=', product_1.id),
('state', '=', 'confirmed')
])
self.assertAlmostEqual(mo.move_finished_ids.date, mo.move_raw_ids.date + timedelta(hours=1), delta=timedelta(seconds=1))
self.assertEqual(len(mo), 1, 'the manufacture order is not created')
mo_form = Form(mo)
self.assertEqual(mo_form.product_qty, 10, 'the quantity to produce is not good relative to the move')
mo = mo_form.save()
# Confirming mo create finished move
move_orig = self.env['stock.move'].search([
('move_dest_ids', 'in', move_dest.ids)
], limit=1)
self.assertEqual(len(move_orig), 1, 'the move orig is not created')
self.assertEqual(move_orig.product_qty, 10, 'the quantity to produce is not good relative to the move')
new_sheduled_date = fields.Datetime.to_datetime(mo.date_planned_start) + timedelta(days=5)
mo.date_planned_start = new_sheduled_date
self.assertAlmostEqual(mo.move_raw_ids.date, mo.date_planned_start, delta=timedelta(seconds=1))
self.assertAlmostEqual(mo.move_finished_ids.date, mo.date_planned_finished, delta=timedelta(seconds=1))
def test_finished_move_cancellation(self):
"""Check state of finished move on cancellation of raw moves. """
product_bottle = self.env['product.product'].create({
'name': 'Plastic Bottle',
'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))]
})
component_mold = self.env['product.product'].create({
'name': 'Plastic Mold',
})
self.env['mrp.bom'].create({
'product_id': product_bottle.id,
'product_tmpl_id': product_bottle.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_mold.id, 'product_qty': 1}),
]})
move_dest = self.env['stock.move'].create({
'name': 'move_bottle',
'product_id': product_bottle.id,
'product_uom': self.ref('uom.product_uom_unit'),
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.ref('stock.stock_location_output'),
'product_uom_qty': 10,
'procure_method': 'make_to_order',
})
move_dest._action_confirm()
mo = self.env['mrp.production'].search([
('product_id', '=', product_bottle.id),
('state', '=', 'confirmed')
])
mo.move_raw_ids[0]._action_cancel()
self.assertEqual(mo.state, 'cancel', 'Manufacturing order should be cancelled.')
self.assertEqual(mo.move_finished_ids[0].state, 'cancel', 'Finished move should be cancelled if mo is cancelled.')
self.assertEqual(mo.move_dest_ids[0].state, 'waiting', 'Destination move should not be cancelled if prapogation cancel is False on manufacturing rule.')
def test_procurement_with_empty_bom(self):
"""Ensure that a procurement request using a product with an empty BoM
will create an empty MO in draft state that can be completed afterwards.
"""
self.warehouse = self.env.ref('stock.warehouse0')
route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
route_mto = self.warehouse.mto_pull_id.route_id.id
product = self.env['product.product'].create({
'name': 'Clafoutis',
'route_ids': [(6, 0, [route_manufacture, route_mto])]
})
self.env['mrp.bom'].create({
'product_id': product.id,
'product_tmpl_id': product.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'type': 'normal',
})
move_dest = self.env['stock.move'].create({
'name': 'Customer MTO Move',
'product_id': product.id,
'product_uom': self.ref('uom.product_uom_unit'),
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.ref('stock.stock_location_output'),
'product_uom_qty': 10,
'procure_method': 'make_to_order',
})
move_dest._action_confirm()
production = self.env['mrp.production'].search([('product_id', '=', product.id)])
self.assertTrue(production)
self.assertFalse(production.move_raw_ids)
self.assertEqual(production.state, 'draft')
comp1 = self.env['product.product'].create({
'name': 'egg',
})
move_values = production._get_move_raw_values(comp1, 40.0, self.env.ref('uom.product_uom_unit'))
self.env['stock.move'].create(move_values)
production.action_confirm()
produce_form = Form(production)
produce_form.qty_producing = production.product_qty
production = produce_form.save()
production.button_mark_done()
move_dest._action_assign()
self.assertEqual(move_dest.reserved_availability, 10.0)
def test_auto_assign(self):
""" When auto reordering rule exists, check for when:
1. There is not enough of a manufactured product to assign (reserve for) a picking => auto-create 1st MO
2. There is not enough of a manufactured component to assign the created MO => auto-create 2nd MO
3. Add an extra manufactured component (not in stock) to 1st MO => auto-create 3rd MO
4. When 2nd MO is completed => auto-assign to 1st MO
5. When 1st MO is completed => auto-assign to picking
6. Additionally check that a MO that has component in stock auto-reserves when MO is confirmed (since default setting = 'at_confirm')"""
self.warehouse = self.env.ref('stock.warehouse0')
route_manufacture = self.warehouse.manufacture_pull_id.route_id
product_1 = self.env['product.product'].create({
'name': 'Cake',
'type': 'product',
'route_ids': [(6, 0, [route_manufacture.id])]
})
product_2 = self.env['product.product'].create({
'name': 'Cake Mix',
'type': 'product',
'route_ids': [(6, 0, [route_manufacture.id])]
})
product_3 = self.env['product.product'].create({
'name': 'Flour',
'type': 'consu',
})
bom1 = self.env['mrp.bom'].create({
'product_id': product_1.id,
'product_tmpl_id': product_1.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1,
'consumption': 'flexible',
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_2.id, 'product_qty': 1}),
]})
self.env['mrp.bom'].create({
'product_id': product_2.id,
'product_tmpl_id': product_2.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_3.id, 'product_qty': 1}),
]})
# extra manufactured component added to 1st MO after it is already confirmed
product_4 = self.env['product.product'].create({
'name': 'Flavor Enchancer',
'type': 'product',
'route_ids': [(6, 0, [route_manufacture.id])]
})
product_5 = self.env['product.product'].create({
'name': 'MSG',
'type': 'consu',
})
self.env['mrp.bom'].create({
'product_id': product_4.id,
'product_tmpl_id': product_4.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_5.id, 'product_qty': 1}),
]})
# setup auto orderpoints (reordering rules)
self.env['stock.warehouse.orderpoint'].create({
'name': 'Cake RR',
'location_id': self.warehouse.lot_stock_id.id,
'product_id': product_1.id,
'product_min_qty': 0,
'product_max_qty': 5,
})
self.env['stock.warehouse.orderpoint'].create({
'name': 'Cake Mix RR',
'location_id': self.warehouse.lot_stock_id.id,
'product_id': product_2.id,
'product_min_qty': 0,
'product_max_qty': 5,
})
self.env['stock.warehouse.orderpoint'].create({
'name': 'Flavor Enchancer RR',
'location_id': self.warehouse.lot_stock_id.id,
'product_id': product_4.id,
'product_min_qty': 0,
'product_max_qty': 5,
})
# create picking output to trigger creating MO for reordering product_1
pick_output = self.env['stock.picking'].create({
'name': 'Cake Delivery Order',
'picking_type_id': self.ref('stock.picking_type_out'),
'location_id': self.warehouse.lot_stock_id.id,
'location_dest_id': self.ref('stock.stock_location_customers'),
'move_ids': [(0, 0, {
'name': '/',
'product_id': product_1.id,
'product_uom': product_1.uom_id.id,
'product_uom_qty': 10.00,
'procure_method': 'make_to_stock',
'location_id': self.warehouse.lot_stock_id.id,
'location_dest_id': self.ref('stock.stock_location_customers'),
})],
})
pick_output.action_confirm() # should trigger orderpoint to create and confirm 1st MO
pick_output.action_assign()
mo = self.env['mrp.production'].search([
('product_id', '=', product_1.id),
('state', '=', 'confirmed')
])
self.assertEqual(len(mo), 1, "Manufacture order was not automatically created")
mo.action_assign()
mo.is_locked = False
self.assertEqual(mo.move_raw_ids.reserved_availability, 0, "No components should be reserved yet")
self.assertEqual(mo.product_qty, 15, "Quantity to produce should be picking demand + reordering rule max qty")
# 2nd MO for product_2 should have been created and confirmed when 1st MO for product_1 was confirmed
mo2 = self.env['mrp.production'].search([
('product_id', '=', product_2.id),
('state', '=', 'confirmed')
])
self.assertEqual(len(mo2), 1, 'Second manufacture order was not created')
self.assertEqual(mo2.product_qty, 20, "Quantity to produce should be MO's 'to consume' qty + reordering rule max qty")
mo2_form = Form(mo2)
mo2_form.qty_producing = 20
mo2 = mo2_form.save()
mo2.button_mark_done()
self.assertEqual(mo.move_raw_ids.reserved_availability, 15, "Components should have been auto-reserved")
# add new component to 1st MO
mo_form = Form(mo)
with mo_form.move_raw_ids.new() as line:
line.product_id = product_4
line.product_uom_qty = 1
mo_form.save() # should trigger orderpoint to create and confirm 3rd MO
mo3 = self.env['mrp.production'].search([
('product_id', '=', product_4.id),
('state', '=', 'confirmed')
])
self.assertEqual(len(mo3), 1, 'Third manufacture order for added component was not created')
self.assertEqual(mo3.product_qty, 6, "Quantity to produce should be 1 + reordering rule max qty")
mo_form = Form(mo)
mo.move_raw_ids.quantity_done = 15
mo_form.qty_producing = 15
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(pick_output.move_ids_without_package.reserved_availability, 10, "Completed products should have been auto-reserved in picking")
# make sure next MO auto-reserves components now that they are in stock since
# default reservation_method = 'at_confirm'
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_1
mo_form.bom_id = bom1
mo_form.product_qty = 5
mo_form.product_uom_id = product_1.uom_id
mo_assign_at_confirm = mo_form.save()
mo_assign_at_confirm.action_confirm()
self.assertEqual(mo_assign_at_confirm.move_raw_ids.reserved_availability, 5, "Components should have been auto-reserved")
def test_check_update_qty_mto_chain(self):
""" Simulate a mto chain with a manufacturing order. Updating the
initial demand should also impact the initial move but not the
linked manufacturing order.
"""
def create_run_procurement(product, product_qty, values=None):
if not values:
values = {
'warehouse_id': picking_type_out.warehouse_id,
'action': 'pull_push',
'group_id': procurement_group,
}
return self.env['procurement.group'].run([self.env['procurement.group'].Procurement(
product, product_qty, self.uom_unit, vendor.property_stock_customer,
product.name, '/', self.env.company, values)
])
picking_type_out = self.env.ref('stock.picking_type_out')
vendor = self.env['res.partner'].create({
'name': 'Roger'
})
# This needs to be tried with MTO route activated
self.env['stock.route'].browse(self.ref('stock.route_warehouse0_mto')).action_unarchive()
# Define products requested for this BoM.
product = self.env['product.product'].create({
'name': 'product',
'type': 'product',
'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('mrp.route_warehouse0_manufacture'))],
'categ_id': self.env.ref('product.product_category_all').id
})
component = self.env['product.product'].create({
'name': 'component',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id
})
self.env['mrp.bom'].create({
'product_id': product.id,
'product_tmpl_id': product.product_tmpl_id.id,
'product_uom_id': product.uom_id.id,
'product_qty': 1.0,
'consumption': 'flexible',
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': component.id, 'product_qty': 1}),
]
})
procurement_group = self.env['procurement.group'].create({
'move_type': 'direct',
'partner_id': vendor.id
})
# Create initial procurement that will generate the initial move and its picking.
create_run_procurement(product, 10, {
'group_id': procurement_group,
'warehouse_id': picking_type_out.warehouse_id,
'partner_id': vendor
})
customer_move = self.env['stock.move'].search([('group_id', '=', procurement_group.id)])
manufacturing_order = self.env['mrp.production'].search([('product_id', '=', product.id)])
self.assertTrue(manufacturing_order, 'No manufacturing order created.')
# Check manufacturing order data.
self.assertEqual(manufacturing_order.product_qty, 10, 'The manufacturing order qty should be the same as the move.')
# Create procurement to decrease quantity in the initial move but not in the related MO.
create_run_procurement(product, -5.00)
self.assertEqual(customer_move.product_uom_qty, 5, 'The demand on the initial move should have been decreased when merged with the procurement.')
self.assertEqual(manufacturing_order.product_qty, 10, 'The demand on the manufacturing order should not have been decreased.')
# Create procurement to increase quantity on the initial move and should create a new MO for the missing qty.
create_run_procurement(product, 2.00)
self.assertEqual(customer_move.product_uom_qty, 5, 'The demand on the initial move should not have been increased since it should be a new move.')
self.assertEqual(manufacturing_order.product_qty, 10, 'The demand on the initial manufacturing order should not have been increased.')
manufacturing_orders = self.env['mrp.production'].search([('product_id', '=', product.id)])
self.assertEqual(len(manufacturing_orders), 2, 'A new MO should have been created for missing demand.')
def test_rr_with_dependance_between_bom(self):
self.warehouse = self.env.ref('stock.warehouse0')
route_mto = self.warehouse.mto_pull_id.route_id
route_mto.active = True
route_manufacture = self.warehouse.manufacture_pull_id.route_id
product_1 = self.env['product.product'].create({
'name': 'Product A',
'type': 'product',
'route_ids': [(6, 0, [route_manufacture.id])]
})
product_2 = self.env['product.product'].create({
'name': 'Product B',
'type': 'product',
'route_ids': [(6, 0, [route_manufacture.id, route_mto.id])]
})
product_3 = self.env['product.product'].create({
'name': 'Product B',
'type': 'product',
'route_ids': [(6, 0, [route_manufacture.id])]
})
product_4 = self.env['product.product'].create({
'name': 'Product C',
'type': 'consu',
})
op1 = self.env['stock.warehouse.orderpoint'].create({
'name': 'Product A',
'location_id': self.warehouse.lot_stock_id.id,
'product_id': product_1.id,
'product_min_qty': 1,
'product_max_qty': 20,
})
op2 = self.env['stock.warehouse.orderpoint'].create({
'name': 'Product B',
'location_id': self.warehouse.lot_stock_id.id,
'product_id': product_3.id,
'product_min_qty': 5,
'product_max_qty': 50,
})
self.env['mrp.bom'].create({
'product_id': product_1.id,
'product_tmpl_id': product_1.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1,
'consumption': 'flexible',
'type': 'normal',
'bom_line_ids': [(0, 0, {'product_id': product_2.id, 'product_qty': 1})]
})
self.env['mrp.bom'].create({
'product_id': product_2.id,
'product_tmpl_id': product_2.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1,
'consumption': 'flexible',
'type': 'normal',
'bom_line_ids': [(0, 0, {'product_id': product_3.id, 'product_qty': 1})]
})
self.env['mrp.bom'].create({
'product_id': product_3.id,
'product_tmpl_id': product_3.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1,
'consumption': 'flexible',
'type': 'normal',
'bom_line_ids': [(0, 0, {'product_id': product_4.id, 'product_qty': 1})]
})
(op1 | op2)._procure_orderpoint_confirm()
mo1 = self.env['mrp.production'].search([('product_id', '=', product_1.id)])
mo3 = self.env['mrp.production'].search([('product_id', '=', product_3.id)])
self.assertEqual(len(mo1), 1)
self.assertEqual(len(mo3), 1)
self.assertEqual(mo1.product_qty, 20)
self.assertEqual(mo3.product_qty, 50)
def test_several_boms_same_finished_product(self):
"""
Suppose a product with two BoMs, each one based on a different operation type
This test ensures that, when running the scheduler, the generated MOs are based
on the correct BoMs
"""
# Required for `picking_type_id` to be visible in the view
self.env.user.groups_id += self.env.ref('stock.group_adv_location')
warehouse = self.env.ref('stock.warehouse0')
stock_location01 = warehouse.lot_stock_id
stock_location02 = stock_location01.copy()
manu_operation01 = warehouse.manu_type_id
manu_operation02 = manu_operation01.copy()
with Form(manu_operation02) as form:
form.name = 'Manufacturing 02'
form.sequence_code = 'MO2'
form.default_location_dest_id = stock_location02
manu_rule01 = warehouse.manufacture_pull_id
manu_route = manu_rule01.route_id
manu_rule02 = manu_rule01.copy()
with Form(manu_rule02) as form:
form.picking_type_id = manu_operation02
manu_route.rule_ids = [(6, 0, (manu_rule01 + manu_rule02).ids)]
compo01, compo02, finished = self.env['product.product'].create([{
'name': 'compo 01',
'type': 'consu',
}, {
'name': 'compo 02',
'type': 'consu',
}, {
'name': 'finished',
'type': 'product',
'route_ids': [(6, 0, manu_route.ids)],
}])
bom01_form = Form(self.env['mrp.bom'])
bom01_form.product_tmpl_id = finished.product_tmpl_id
bom01_form.code = '01'
bom01_form.picking_type_id = manu_operation01
with bom01_form.bom_line_ids.new() as line:
line.product_id = compo01
bom01 = bom01_form.save()
bom02_form = Form(self.env['mrp.bom'])
bom02_form.product_tmpl_id = finished.product_tmpl_id
bom02_form.code = '02'
bom02_form.picking_type_id = manu_operation02
with bom02_form.bom_line_ids.new() as line:
line.product_id = compo02
bom02 = bom02_form.save()
self.env['stock.warehouse.orderpoint'].create([{
'warehouse_id': warehouse.id,
'location_id': stock_location01.id,
'product_id': finished.id,
'product_min_qty': 1,
'product_max_qty': 1,
}, {
'warehouse_id': warehouse.id,
'location_id': stock_location02.id,
'product_id': finished.id,
'product_min_qty': 2,
'product_max_qty': 2,
}])
self.env['procurement.group'].run_scheduler()
mos = self.env['mrp.production'].search([('product_id', '=', finished.id)], order='origin')
self.assertRecordValues(mos, [
{'product_qty': 1, 'bom_id': bom01.id, 'picking_type_id': manu_operation01.id, 'location_dest_id': stock_location01.id},
{'product_qty': 2, 'bom_id': bom02.id, 'picking_type_id': manu_operation02.id, 'location_dest_id': stock_location02.id},
])
def test_pbm_and_additionnal_components(self):
"""
2-steps manufacturring.
When adding a new component to a confirmed MO, it should add an SM in
the PBM picking. Also, it should be possible to define the to-consume
qty of the new line even if the MO is locked
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
warehouse.manufacture_steps = 'pbm'
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_4
mo = mo_form.save()
mo.action_confirm()
if not mo.is_locked:
mo.action_toggle_is_locked()
with Form(mo) as mo_form:
with mo_form.move_raw_ids.new() as raw_line:
raw_line.product_id = self.product_2
raw_line.product_uom_qty = 2.0
move_vals = mo._get_move_raw_values(self.product_3, 0, self.product_3.uom_id)
mo.move_raw_ids = [(0, 0, move_vals)]
mo.move_raw_ids[-1].product_uom_qty = 3.0
expected_vals = [
{'product_id': self.product_1.id, 'product_uom_qty': 1.0},
{'product_id': self.product_2.id, 'product_uom_qty': 2.0},
{'product_id': self.product_3.id, 'product_uom_qty': 3.0},
]
self.assertRecordValues(mo.move_raw_ids, expected_vals)
self.assertRecordValues(mo.picking_ids.move_ids, expected_vals)

View file

@ -0,0 +1,269 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tests import Form
from odoo import Command
class TestMrpSerialMassProduce(TestMrpCommon):
def test_smp_serial(self):
"""Create a MO for a product not tracked by serial number.
The smp wizard should not open.
"""
mo = self.generate_mo()[0]
self.assertEqual(mo.state, 'confirmed')
res = mo.action_serial_mass_produce_wizard()
self.assertFalse(res)
def test_smp_produce_all(self):
"""Create a MO for a product tracked by serial number.
Open the smp wizard, generate all serial numbers to produce all quantities.
"""
mo = self.generate_mo(tracking_final='serial')[0]
count = mo.product_qty
# Make some stock and reserve
for product in mo.move_raw_ids.product_id:
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id,
'inventory_quantity': 100,
'location_id': mo.location_src_id.id,
})._apply_inventory()
mo.action_assign()
# Open the wizard
action = mo.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
# Let the wizard generate all serial numbers
wizard.next_serial_number = "sn#1"
wizard.next_serial_count = count
action = wizard.save().generate_serial_numbers_production()
# Reload the wizard to apply generated serial numbers
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard.save().apply()
# Initial MO should have a backorder-sequenced name and be in to_close state
self.assertTrue("-001" in mo.name)
self.assertEqual(mo.state, "to_close")
# Each generated serial number should have its own mo
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), count)
# Check generated serial numbers
self.assertEqual(mo.procurement_group_id.mrp_production_ids.lot_producing_id.mapped('name'), ["sn#1", "sn#2", "sn#3", "sn#4", "sn#5"])
def test_smp_produce_all_but_one(self):
"""Create a MO for a product tracked by serial number.
Open the smp wizard, generate all but one serial numbers and create a back order.
"""
mo = self.generate_mo(tracking_final='serial')[0]
count = mo.product_qty
# Make some stock and reserve
for product in mo.move_raw_ids.product_id:
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id,
'inventory_quantity': 100,
'location_id': mo.location_src_id.id,
})._apply_inventory()
mo.action_assign()
action = mo.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
wizard.next_serial_number = "sn#1"
wizard.next_serial_count = count - 1
action = wizard.save().generate_serial_numbers_production()
# Reload the wizard to create backorder (applying generated serial numbers)
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard.save().create_backorder()
# Last MO in sequence is the backorder
bo = mo.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(bo.backorder_sequence, count)
self.assertEqual(bo.state, "confirmed")
def test_smp_produce_complex(self):
"""Create a MO for a product tracked by serial number
and with complex components (serial and multiple lots).
Open the smp wizard, generate all serial numbers to produce all quantities.
Check lot splitting.
"""
mo, dummy, dummy, product_to_use_1, product_to_use_2 = self.generate_mo(tracking_final='serial', tracking_base_1='lot', tracking_base_2='serial', qty_final=3, qty_base_1=2, qty_base_2=1)
count = mo.product_qty
# Make some stock and reserve
for _ in range(2): # 2 lots of 3 to satisfy the need and check lot splitting
lot = self.env['stock.lot'].create({
'product_id': product_to_use_1.id,
'company_id': self.env.company.id,
})
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product_to_use_1.id,
'inventory_quantity': 3,
'location_id': mo.location_src_id.id,
'lot_id': lot.id,
})._apply_inventory()
for _ in range(3): # 3 serial numbers
lot = self.env['stock.lot'].create({
'product_id': product_to_use_2.id,
'company_id': self.env.company.id,
})
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product_to_use_2.id,
'inventory_quantity': 1,
'location_id': mo.location_src_id.id,
'lot_id': lot.id,
})._apply_inventory()
mo.action_assign()
# Open the wizard
action = mo.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
# Let the wizard generate all serial numbers
wizard.next_serial_number = "sn#1"
wizard.next_serial_count = count
action = wizard.save().generate_serial_numbers_production()
# Reload the wizard to apply generated serial numbers
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard.save().apply()
# 1st & 3rd MO in sequence should have only 1 move lines (1 lot) for product_to_use_1 (2nd in bom)
self.assertEqual(mo.procurement_group_id.mrp_production_ids[0].move_raw_ids[1].move_lines_count, 1)
self.assertEqual(mo.procurement_group_id.mrp_production_ids[2].move_raw_ids[1].move_lines_count, 1)
# 2nd MO should have 2 move lines (2 different lots) for product_to_use_1
self.assertEqual(mo.procurement_group_id.mrp_production_ids[1].move_raw_ids[1].move_lines_count, 2)
def test_mass_produce_with_tracked_product(self):
"""
Check that we can mass produce a tracked product.
"""
tracked_product = self.env['product.product'].create({
'name': 'Tracked Product',
'type': 'product',
'tracking': 'serial',
})
component = self.env['product.product'].create({
'name': 'Component',
'type': 'product',
})
byproduct = self.env['product.product'].create({
'name': 'Byproduct',
'type': 'product',
})
# create a BoM
bom = self.env['mrp.bom'].create({
'product_tmpl_id': tracked_product.product_tmpl_id.id,
'product_qty': 1,
'bom_line_ids': [Command.create({
'product_id': component.id,
'product_qty': 1,
})],
'byproduct_ids': [Command.create({
'product_id': byproduct.id,
'product_qty': 1,
})],
})
sn_1 = self.env['stock.lot'].create({
'name': 'SN1',
'product_id': tracked_product.id,
})
sn_2 = self.env['stock.lot'].create({
'name': 'SN2',
'product_id': tracked_product.id,
})
self.env['stock.quant']._update_available_quantity(tracked_product, self.stock_location_14, 1, lot_id=sn_1)
self.env['stock.quant']._update_available_quantity(tracked_product, self.stock_location_14, 1, lot_id=sn_2)
self.env['stock.quant']._update_available_quantity(component, self.stock_location_14, 10)
# create an MO to use the tracked product available in stock
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_1
mo_form.product_qty = 2
mo_form.product_uom_id = component.uom_id
# use tracked as component
with mo_form.move_raw_ids.new() as move:
move.name = tracked_product.name
move.product_id = tracked_product
move.product_uom_qty = 2
move.product_uom = tracked_product.uom_id
mo = mo_form.save()
mo.action_confirm()
mo.action_assign()
mo.qty_producing = 2
mo.move_raw_ids.move_line_ids.write({'qty_done': 1})
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
# create a Mo to produce 2 units of tracked product
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = tracked_product
mo_form.bom_id = bom
mo_form.product_qty = 2
mo_form.product_uom_id = tracked_product.uom_id
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
mo.action_assign()
# Open the wizard
action = mo.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
# Let the wizard generate all serial numbers
wizard.next_serial_number = "sn#3"
wizard.next_serial_count = 2
action = wizard.save().generate_serial_numbers_production()
# Reload the wizard to apply generated serial numbers
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard.save().apply()
# Initial MO should have a backorder-sequenced name and be in to_close state
self.assertTrue("-001" in mo.name)
self.assertEqual(mo.state, "to_close")
# Each generated serial number should have its own mo
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 2)
# Check generated serial numbers
self.assertEqual(mo.procurement_group_id.mrp_production_ids.lot_producing_id.mapped('name'), ["sn#3", "sn#4"])
#check byproduct quantity
self.assertEqual(mo.procurement_group_id.mrp_production_ids.move_byproduct_ids.mapped('quantity_done'), [1, 1])
# check the component quantity
self.assertEqual(mo.procurement_group_id.mrp_production_ids.move_raw_ids.mapped('quantity_done'), [1, 1])
# Mark the MOs as done
mo.procurement_group_id.mrp_production_ids.button_mark_done()
self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped('state'), ['done', 'done'])
def test_smp_produce_with_consumable_component(self):
"""Create a MO for a product tracked by serial number with a consumable component.
Open the smp wizard, You should be able to generate all serial numbers.
BoM:
- 1x final product (tracked by serial number)
components:
- 2 x (storable)
- 4 x (consumable)
- Create a MO with 12 final products to produce.
- update the component quantity to 100
"""
self.bom_1.product_id.uom_id = self.ref('uom.product_uom_unit')
self.bom_1.product_id.tracking = 'serial'
self.bom_1.product_qty = 1
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_1
mo_form.product_qty = 12
mo = mo_form.save()
self.assertEqual(mo.move_raw_ids.mapped(lambda l: l.product_qty), [24, 48])
self.assertEqual(mo.move_raw_ids[1].product_id.type, 'consu', 'The second component should be consumable')
mo.move_raw_ids[1].product_uom_qty = 100
# Make some stock and reserve for storable component
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': mo.move_raw_ids[0].product_id.id,
'inventory_quantity': 24,
'location_id': mo.location_src_id.id,
})._apply_inventory()
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
# Open the wizard
action = mo.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
# Let the wizard generate all serial numbers
wizard.next_serial_number = "sn#1"
wizard.next_serial_count = mo.product_qty
action = wizard.save().generate_serial_numbers_production()
# Reload the wizard to apply generated serial numbers
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard.save().apply()
# Initial MO should have a backorder-sequenced name and be in to_close state
self.assertTrue("-001" in mo.name)
self.assertEqual(mo.state, "to_close")
# Each generated serial number should have its own mo
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 12)

View file

@ -0,0 +1,524 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from odoo import Command
from odoo.exceptions import UserError
from odoo.tests import Form
class TestWarehouseMrp(common.TestMrpCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
unit = cls.env.ref("uom.product_uom_unit")
cls.stock_location = cls.env.ref('stock.stock_location_stock')
cls.depot_location = cls.env['stock.location'].create({
'name': 'Depot',
'usage': 'internal',
'location_id': cls.stock_location.id,
})
cls.env["stock.putaway.rule"].create({
"location_in_id": cls.stock_location.id,
"location_out_id": cls.depot_location.id,
'category_id': cls.env.ref('product.product_category_all').id,
})
cls.env['mrp.workcenter'].create({
'name': 'Assembly Line 1',
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
})
cls.env['stock.quant'].create({
'location_id': cls.stock_location_14.id,
'product_id': cls.graphics_card.id,
'inventory_quantity': 16.0
}).action_apply_inventory()
cls.bom_laptop = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.laptop.product_tmpl_id.id,
'product_qty': 1,
'product_uom_id': unit.id,
'consumption': 'flexible',
'bom_line_ids': [(0, 0, {
'product_id': cls.graphics_card.id,
'product_qty': 1,
'product_uom_id': unit.id
})],
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
],
})
def new_mo_laptop(self):
form = Form(self.env['mrp.production'])
form.product_id = self.laptop
form.product_qty = 1
form.bom_id = self.bom_laptop
p = form.save()
p.action_confirm()
p.action_assign()
return p
def test_manufacturing_route(self):
warehouse_1_stock_manager = self.warehouse_1.with_user(self.user_stock_manager)
manu_rule = self.env['stock.rule'].search([
('action', '=', 'manufacture'),
('warehouse_id', '=', self.warehouse_1.id)])
self.assertEqual(self.warehouse_1.manufacture_pull_id, manu_rule)
manu_route = manu_rule.route_id
self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
warehouse_1_stock_manager.write({
'manufacture_to_resupply': False
})
self.assertFalse(self.warehouse_1.manufacture_pull_id.active)
self.assertFalse(self.warehouse_1.manu_type_id.active)
self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes())
warehouse_1_stock_manager.write({
'manufacture_to_resupply': True
})
manu_rule = self.env['stock.rule'].search([
('action', '=', 'manufacture'),
('warehouse_id', '=', self.warehouse_1.id)])
self.assertEqual(self.warehouse_1.manufacture_pull_id, manu_rule)
self.assertTrue(self.warehouse_1.manu_type_id.active)
self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
def test_manufacturing_scrap(self):
"""
Testing to do a scrap of consumed material.
"""
# Update demo products
(self.product_4 | self.product_2).write({
'tracking': 'lot',
})
# Update Bill Of Material to remove product with phantom bom.
self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
# Create Inventory Adjustment For Stick and Stone Tools with lot.
lot_product_4 = self.env['stock.lot'].create({
'name': '0000000000001',
'product_id': self.product_4.id,
'company_id': self.env.company.id,
})
lot_product_2 = self.env['stock.lot'].create({
'name': '0000000000002',
'product_id': self.product_2.id,
'company_id': self.env.company.id,
})
# Inventory for Stick
self.env['stock.quant'].create({
'location_id': self.stock_location_14.id,
'product_id': self.product_4.id,
'inventory_quantity': 8,
'lot_id': lot_product_4.id
}).action_apply_inventory()
# Inventory for Stone Tools
self.env['stock.quant'].create({
'location_id': self.stock_location_14.id,
'product_id': self.product_2.id,
'inventory_quantity': 12,
'lot_id': lot_product_2.id
}).action_apply_inventory()
#Create Manufacturing order.
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.product_6
production_form.bom_id = self.bom_3
production_form.product_qty = 12
production_form.product_uom_id = self.product_6.uom_id
production_3 = production_form.save()
production_3.action_confirm()
production_3.action_assign()
# Check Manufacturing order's availability.
self.assertEqual(production_3.reservation_state, 'assigned', "Production order's availability should be Available.")
location_id = production_3.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) and production_3.location_src_id.id or production_3.location_dest_id.id,
# Scrap Product Wood without lot to check assert raise ?.
scrap_id = self.env['stock.scrap'].with_context(active_model='mrp.production', active_id=production_3.id).create({'product_id': self.product_2.id, 'scrap_qty': 1.0, 'product_uom_id': self.product_2.uom_id.id, 'location_id': location_id, 'production_id': production_3.id})
with self.assertRaises(UserError):
scrap_id.do_scrap()
# Scrap Product Wood with lot.
scrap_id = self.env['stock.scrap'].with_context(active_model='mrp.production', active_id=production_3.id).create({'product_id': self.product_2.id, 'scrap_qty': 1.0, 'product_uom_id': self.product_2.uom_id.id, 'location_id': location_id, 'lot_id': lot_product_2.id, 'production_id': production_3.id})
scrap_id.do_scrap()
scrap_move = scrap_id.move_id
self.assertTrue(scrap_move.raw_material_production_id)
self.assertTrue(scrap_move.scrapped)
self.assertEqual(scrap_move.location_dest_id, scrap_id.scrap_location_id)
self.assertEqual(scrap_move.price_unit, scrap_move.product_id.standard_price)
#Check scrap move is created for this production order.
#TODO: should check with scrap objects link in between
# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.scrapped)
# self.assertTrue(scrap_move, "There are no any scrap move created for production order.")
def test_putaway_after_manufacturing_3(self):
""" This test checks a tracked manufactured product will go to location
defined in putaway strategy when the production is recorded with
product.produce wizard.
"""
self.laptop.tracking = 'serial'
mo_laptop = self.new_mo_laptop()
serial = self.env['stock.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id})
mo_form = Form(mo_laptop)
mo_form.qty_producing = 1
mo_form.lot_producing_id = serial
mo_laptop = mo_form.save()
mo_laptop.button_mark_done()
# We check if the laptop go in the depot and not in the stock
move = mo_laptop.move_finished_ids
location_dest = move.move_line_ids.location_dest_id
self.assertEqual(location_dest.id, self.depot_location.id)
self.assertNotEqual(location_dest.id, self.stock_location.id)
def test_backorder_unpacking(self):
""" Test that movement of pack in backorder is correctly handled. """
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
warehouse.write({'manufacture_steps': 'pbm'})
self.product_1.type = 'product'
self.env['stock.quant']._update_available_quantity(self.product_1, self.stock_location, 100)
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_4
mo_form.product_qty = 100
mo = mo_form.save()
mo.action_confirm()
package = self.env['stock.quant.package'].create({})
picking = mo.picking_ids
picking.move_line_ids.write({
'qty_done': 20,
'result_package_id': package.id,
})
res_dict = picking.button_validate()
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
wizard.process()
backorder = picking.backorder_ids
backorder.move_line_ids.qty_done = 80
backorder.button_validate()
self.assertEqual(picking.state, 'done')
self.assertEqual(backorder.state, 'done')
self.assertEqual(mo.move_raw_ids.move_line_ids.mapped('reserved_qty'), [20, 80])
def test_unarchive_mto_route_active_needed_rules_only(self):
""" Ensure that activating a route will activate only its relevant rules.
Here, unarchiving the MTO route shouldn't active pull rule for the Pre-Production
location if manufacture is in 1 step since this location is archived.
"""
self.env.user.groups_id += self.env.ref('stock.group_adv_location')
mto_route = self.env.ref('stock.route_warehouse0_mto')
# initially 'WH: Stock → Pre-Production (MTO)' is inactive and not shown in MTO route.
self.assertEqual(self.warehouse_1.manufacture_steps, 'mrp_one_step')
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
self.assertFalse(mto_route.active)
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
# Activate the MTO route and still 'WH: Stock → Pre-Production (MTO)' is not shown in MTO route.
mto_route.active = True
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
# Change MRP steps mrp_one_step to pbm_sam and now that rule is shown in mto route.
self.warehouse_1.manufacture_steps = 'pbm_sam'
self.assertTrue(self.warehouse_1.pbm_mto_pull_id.active)
self.assertTrue(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
self.assertIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
# Revert to mrp_one_step MRP and confirm rules visibility is updated correctly
self.warehouse_1.manufacture_steps = 'mrp_one_step'
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
class TestKitPicking(common.TestMrpCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
def create_product(name):
p = Form(cls.env['product.product'])
p.name = name
p.detailed_type = 'product'
return p.save()
# Create a kit 'kit_parent' :
# ---------------------------
#
# kit_parent --|- kit_2 x2 --|- component_d x1
# | |- kit_1 x2 -------|- component_a x2
# | |- component_b x1
# | |- component_c x3
# |
# |- kit_3 x1 --|- component_f x1
# | |- component_g x2
# |
# |- component_e x1
# Creating all components
component_a = create_product('Comp A')
component_b = create_product('Comp B')
component_c = create_product('Comp C')
component_d = create_product('Comp D')
component_e = create_product('Comp E')
component_f = create_product('Comp F')
component_g = create_product('Comp G')
# Creating all kits
kit_1 = create_product('Kit 1')
kit_2 = create_product('Kit 2')
kit_3 = create_product('kit 3')
cls.kit_parent = create_product('Kit Parent')
# Linking the kits and the components via some 'phantom' BoMs
bom_kit_1 = cls.env['mrp.bom'].create({
'product_tmpl_id': kit_1.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine = cls.env['mrp.bom.line']
BomLine.create({
'product_id': component_a.id,
'product_qty': 2.0,
'bom_id': bom_kit_1.id})
BomLine.create({
'product_id': component_b.id,
'product_qty': 1.0,
'bom_id': bom_kit_1.id})
BomLine.create({
'product_id': component_c.id,
'product_qty': 3.0,
'bom_id': bom_kit_1.id})
bom_kit_2 = cls.env['mrp.bom'].create({
'product_tmpl_id': kit_2.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine.create({
'product_id': component_d.id,
'product_qty': 1.0,
'bom_id': bom_kit_2.id})
BomLine.create({
'product_id': kit_1.id,
'product_qty': 2.0,
'bom_id': bom_kit_2.id})
bom_kit_parent = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.kit_parent.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine.create({
'product_id': component_e.id,
'product_qty': 1.0,
'bom_id': bom_kit_parent.id})
BomLine.create({
'product_id': kit_2.id,
'product_qty': 2.0,
'bom_id': bom_kit_parent.id})
bom_kit_3 = cls.env['mrp.bom'].create({
'product_tmpl_id': kit_3.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine.create({
'product_id': component_f.id,
'product_qty': 1.0,
'bom_id': bom_kit_3.id})
BomLine.create({
'product_id': component_g.id,
'product_qty': 2.0,
'bom_id': bom_kit_3.id})
BomLine.create({
'product_id': kit_3.id,
'product_qty': 1.0,
'bom_id': bom_kit_parent.id})
# We create an 'immediate transfer' receipt for x3 kit_parent
cls.test_partner = cls.env['res.partner'].create({
'name': 'Notthat Guyagain',
})
cls.test_supplier = cls.env['stock.location'].create({
'name': 'supplier',
'usage': 'supplier',
'location_id': cls.env.ref('stock.stock_location_stock').id,
})
cls.expected_quantities = {
component_a: 24,
component_b: 12,
component_c: 36,
component_d: 6,
component_e: 3,
component_f: 3,
component_g: 6
}
def test_kit_immediate_transfer(self):
""" Make sure a kit is split in the corrects quantity_done by components in case of an
immediate transfer.
"""
picking = self.env['stock.picking'].create({
'location_id': self.test_supplier.id,
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
'partner_id': self.test_partner.id,
'picking_type_id': self.env.ref('stock.picking_type_in').id,
'immediate_transfer': True
})
move_receipt_1 = self.env['stock.move'].create({
'name': self.kit_parent.name,
'product_id': self.kit_parent.id,
'quantity_done': 3,
'product_uom': self.kit_parent.uom_id.id,
'picking_id': picking.id,
'picking_type_id': self.env.ref('stock.picking_type_in').id,
'location_id': self.test_supplier.id,
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
})
picking.button_validate()
# We check that the picking has the correct quantities after its move were splitted.
self.assertEqual(len(picking.move_ids), 7)
for move_line in picking.move_ids:
self.assertEqual(move_line.quantity_done, self.expected_quantities[move_line.product_id])
def test_kit_planned_transfer(self):
""" Make sure a kit is split in the corrects product_qty by components in case of a
planned transfer.
"""
picking = self.env['stock.picking'].create({
'location_id': self.test_supplier.id,
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
'partner_id': self.test_partner.id,
'picking_type_id': self.env.ref('stock.picking_type_in').id,
'immediate_transfer': False,
})
move_receipt_1 = self.env['stock.move'].create({
'name': self.kit_parent.name,
'product_id': self.kit_parent.id,
'product_uom_qty': 3,
'product_uom': self.kit_parent.uom_id.id,
'picking_id': picking.id,
'picking_type_id': self.env.ref('stock.picking_type_in').id,
'location_id': self.test_supplier.id,
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
})
picking.action_confirm()
# We check that the picking has the correct quantities after its move were splitted.
self.assertEqual(len(picking.move_ids), 7)
for move_line in picking.move_ids:
self.assertEqual(move_line.product_qty, self.expected_quantities[move_line.product_id])
def test_add_sml_with_kit_to_confirmed_picking(self):
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
customer_location = self.env.ref('stock.stock_location_customers')
stock_location = warehouse.lot_stock_id
in_type = warehouse.in_type_id
self.bom_4.type = 'phantom'
kit = self.bom_4.product_id
compo = self.bom_4.bom_line_ids.product_id
product = self.env['product.product'].create({'name': 'Super Product', 'type': 'product'})
receipt = self.env['stock.picking'].create({
'picking_type_id': in_type.id,
'location_id': customer_location.id,
'location_dest_id': stock_location.id,
'move_ids': [(0, 0, {
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'location_id': customer_location.id,
'location_dest_id': stock_location.id,
})]
})
receipt.action_confirm()
receipt.move_line_ids.qty_done = 1
receipt.move_line_ids = [(0, 0, {
'product_id': kit.id,
'qty_done': 1,
'product_uom_id': kit.uom_id.id,
'location_id': customer_location.id,
'location_dest_id': stock_location.id,
})]
receipt.button_validate()
self.assertEqual(receipt.state, 'done')
self.assertRecordValues(receipt.move_ids, [
{'product_id': product.id, 'quantity_done': 1, 'state': 'done'},
{'product_id': compo.id, 'quantity_done': 1, 'state': 'done'},
])
def test_move_line_aggregated_product_quantities_with_kit(self):
""" Test the `stock.move.line` method `_get_aggregated_product_quantities`,
who returns data used to print delivery slips, using kits.
"""
uom_unit = self.env.ref('uom.product_uom_unit')
kit, kit_component_1, kit_component_2, not_kit_1, not_kit_2 = self.env['product.product'].create([{
'name': name,
'type': 'product',
'uom_id': uom_unit.id,
} for name in ['Kit', 'Kit Component 1', 'Kit Component 2', 'Not Kit 1', 'Not Kit 2']])
bom_kit = self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_uom_id': kit.product_tmpl_id.uom_id.id,
'product_id': kit.id,
'product_qty': 1.0,
'type': 'phantom',
'bom_line_ids': [
Command.create({
'product_id': kit_component_1.id,
'product_qty': 1,
}),
Command.create({
'product_id': kit_component_2.id,
'product_qty': 1,
}),
]
})
delivery_form = Form(self.env['stock.picking'])
delivery_form.picking_type_id = self.env.ref('stock.picking_type_in')
with delivery_form.move_ids_without_package.new() as move:
move.product_id = bom_kit.product_id
move.product_uom_qty = 4
with delivery_form.move_ids_without_package.new() as move:
move.product_id = not_kit_1
move.product_uom_qty = 4
with delivery_form.move_ids_without_package.new() as move:
move.product_id = not_kit_2
move.product_uom_qty = 3
delivery = delivery_form.save()
delivery.action_confirm()
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_1).qty_done = 3
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_2).qty_done = 3
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_1).qty_done = 4
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_2).qty_done = 2
backorder_wizard_dict = delivery.button_validate()
backorder_wizard_form = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context']))
backorder_wizard_form.save().process_cancel_backorder()
aggregate_not_kit_values = delivery.move_line_ids._get_aggregated_product_quantities()
self.assertEqual(len(aggregate_not_kit_values.keys()), 2)
self.assertTrue(all('Not' in val for val in aggregate_not_kit_values), 'Only non kit products should be included')
aggregate_kit_values = delivery.move_line_ids._get_aggregated_product_quantities(kit_name=bom_kit.product_id.name)
self.assertEqual(len(aggregate_kit_values.keys()), 2)
self.assertTrue(all('Component' in val for val in aggregate_kit_values), 'Only kit products should be included')

View file

@ -0,0 +1,258 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import Form
from odoo.addons.stock.tests.test_report import TestReportsCommon
class TestMrpStockReports(TestReportsCommon):
def test_report_forecast_1_mo_count(self):
""" Creates and configures a product who could be produce and could be a component.
Plans some producing and consumming MO and check the report values.
"""
# Create a variant attribute.
product_chocolate = self.env['product.product'].create({
'name': 'Chocolate',
'type': 'consu',
})
product_chococake = self.env['product.product'].create({
'name': 'Choco Cake',
'type': 'product',
})
product_double_chococake = self.env['product.product'].create({
'name': 'Double Choco Cake',
'type': 'product',
})
# Creates two BOM: one creating a regular slime, one using regular slimes.
bom_chococake = self.env['mrp.bom'].create({
'product_id': product_chococake.id,
'product_tmpl_id': product_chococake.product_tmpl_id.id,
'product_uom_id': product_chococake.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_chocolate.id, 'product_qty': 4}),
],
})
bom_double_chococake = self.env['mrp.bom'].create({
'product_id': product_double_chococake.id,
'product_tmpl_id': product_double_chococake.product_tmpl_id.id,
'product_uom_id': product_double_chococake.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_chococake.id, 'product_qty': 2}),
],
})
# Creates two MO: one for each BOM.
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_chococake
mo_form.bom_id = bom_chococake
mo_form.product_qty = 10
mo_1 = mo_form.save()
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_double_chococake
mo_form.bom_id = bom_double_chococake
mo_form.product_qty = 2
mo_2 = mo_form.save()
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids)
draft_picking_qty = docs['draft_picking_qty']
draft_production_qty = docs['draft_production_qty']
self.assertEqual(len(lines), 0, "Must have 0 line.")
self.assertEqual(draft_picking_qty['in'], 0)
self.assertEqual(draft_picking_qty['out'], 0)
self.assertEqual(draft_production_qty['in'], 10)
self.assertEqual(draft_production_qty['out'], 4)
# Confirms the MO and checks the report lines.
mo_1.action_confirm()
mo_2.action_confirm()
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids)
draft_picking_qty = docs['draft_picking_qty']
draft_production_qty = docs['draft_production_qty']
self.assertEqual(len(lines), 2, "Must have two line.")
line_1 = lines[0]
line_2 = lines[1]
self.assertEqual(line_1['document_in'].id, mo_1.id)
self.assertEqual(line_1['quantity'], 4)
self.assertEqual(line_1['document_out'].id, mo_2.id)
self.assertEqual(line_2['document_in'].id, mo_1.id)
self.assertEqual(line_2['quantity'], 6)
self.assertEqual(line_2['document_out'], False)
self.assertEqual(draft_picking_qty['in'], 0)
self.assertEqual(draft_picking_qty['out'], 0)
self.assertEqual(draft_production_qty['in'], 0)
self.assertEqual(draft_production_qty['out'], 0)
def test_report_forecast_2_production_backorder(self):
""" Creates a manufacturing order and produces half the quantity.
Then creates a backorder and checks the report.
"""
# Configures the warehouse.
warehouse = self.env.ref('stock.warehouse0')
warehouse.manufacture_steps = 'pbm_sam'
# Configures a product.
product_apple_pie = self.env['product.product'].create({
'name': 'Apple Pie',
'type': 'product',
})
product_apple = self.env['product.product'].create({
'name': 'Apple',
'type': 'consu',
})
bom = self.env['mrp.bom'].create({
'product_id': product_apple_pie.id,
'product_tmpl_id': product_apple_pie.product_tmpl_id.id,
'product_uom_id': product_apple_pie.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_apple.id, 'product_qty': 5}),
],
})
# Creates a MO and validates the pick components.
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_apple_pie
mo_form.bom_id = bom
mo_form.product_qty = 4
mo_1 = mo_form.save()
mo_1.action_confirm()
pick = mo_1.move_raw_ids.move_orig_ids.picking_id
pick.picking_type_id.show_operations = True # Could be false without demo data, as the lot group is disabled
pick_form = Form(pick)
with pick_form.move_line_ids_without_package.edit(0) as move_line:
move_line.qty_done = 20
pick = pick_form.save()
pick.button_validate()
# Produces 3 products then creates a backorder for the remaining product.
mo_form = Form(mo_1)
mo_form.qty_producing = 3
mo_1 = mo_form.save()
action = mo_1.button_mark_done()
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder = backorder_form.save()
backorder.action_backorder()
mo_2 = (mo_1.procurement_group_id.mrp_production_ids - mo_1)
# Checks the forecast report.
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
self.assertEqual(len(lines), 1, "Must have only one line about the backorder")
self.assertEqual(lines[0]['document_in'].id, mo_2.id)
self.assertEqual(lines[0]['quantity'], 1)
self.assertEqual(lines[0]['document_out'], False)
# Produces the last unit.
mo_form = Form(mo_2)
mo_form.qty_producing = 1
mo_2 = mo_form.save()
mo_2.button_mark_done()
# Checks the forecast report.
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
self.assertEqual(len(lines), 0, "Must have no line")
def test_report_forecast_3_report_line_corresponding_to_mo_highlighted(self):
""" When accessing the report from a MO, checks if the correct MO is highlighted in the report
"""
product_banana = self.env['product.product'].create({
'name': 'Banana',
'type': 'product',
})
product_chocolate = self.env['product.product'].create({
'name': 'Chocolate',
'type': 'consu',
})
# We create 2 identical MO
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_banana
mo_form.product_qty = 10
with mo_form.move_raw_ids.new() as move:
move.product_id = product_chocolate
mo_1 = mo_form.save()
mo_2 = mo_1.copy()
(mo_1 | mo_2).action_confirm()
# Check for both MO if the highlight (is_matched) corresponds to the correct MO
for mo in [mo_1, mo_2]:
context = mo.action_product_forecast_report()['context']
_, _, lines = self.get_report_forecast(product_template_ids=product_banana.product_tmpl_id.ids, context=context)
for line in lines:
if line['document_in'] == mo:
self.assertTrue(line['is_matched'], "The corresponding MO line should be matched in the forecast report.")
else:
self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the MO shoud not be matched.")
def test_subkit_in_delivery_slip(self):
"""
Suppose this structure:
Super Kit --|- Compo 01 x1
|- Sub Kit x1 --|- Compo 02 x1
| |- Compo 03 x1
This test ensures that, when delivering one Super Kit, one Sub Kit, one Compo 01 and one Compo 02,
and when putting in pack the third component of the Super Kit, the delivery report is correct.
"""
compo01, compo02, compo03, subkit, superkit = self.env['product.product'].create([{
'name': n,
'type': 'consu',
} for n in ['Compo 01', 'Compo 02', 'Compo 03', 'Sub Kit', 'Super Kit']])
self.env['mrp.bom'].create([{
'product_tmpl_id': subkit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo02.id, 'product_qty': 1}),
(0, 0, {'product_id': compo03.id, 'product_qty': 1}),
],
}, {
'product_tmpl_id': superkit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo01.id, 'product_qty': 1}),
(0, 0, {'product_id': subkit.id, 'product_qty': 1}),
],
}])
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.picking_type_out
picking_form.partner_id = self.partner
with picking_form.move_ids_without_package.new() as move:
move.product_id = superkit
move.product_uom_qty = 1
with picking_form.move_ids_without_package.new() as move:
move.product_id = subkit
move.product_uom_qty = 1
with picking_form.move_ids_without_package.new() as move:
move.product_id = compo01
move.product_uom_qty = 1
with picking_form.move_ids_without_package.new() as move:
move.product_id = compo02
move.product_uom_qty = 1
picking = picking_form.save()
picking.action_confirm()
picking.move_ids.quantity_done = 1
move = picking.move_ids.filtered(lambda m: m.name == "Super Kit" and m.product_id == compo03)
move.move_line_ids.result_package_id = self.env['stock.quant.package'].create({'name': 'Package0001'})
picking.button_validate()
html_report = self.env['ir.actions.report']._render_qweb_html(
'stock.report_deliveryslip', picking.ids)[0].decode('utf-8').split('\n')
keys = [
"Package0001", "Compo 03",
"Products with no package assigned", "Compo 01", "Compo 02",
"Super Kit", "Compo 01",
"Sub Kit", "Compo 02", "Compo 03",
]
for line in html_report:
if not keys:
break
if keys[0] in line:
keys = keys[1:]
self.assertFalse(keys, "All keys should be in the report with the defined order")

View file

@ -0,0 +1,848 @@
# -*- 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
from odoo.exceptions import UserError
import logging
from freezegun import freeze_time
from datetime import datetime
_logger = logging.getLogger(__name__)
class TestTraceability(TestMrpCommon):
TRACKING_TYPES = ['none', 'serial', 'lot']
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
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.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')
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',
}, {
'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)
def test_unbuild_scrap_and_unscrap_tracked_component(self):
"""
Suppose a tracked-by-SN component C. There is one C in stock with SN01.
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',
'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)
# produce 1
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_4
mo = mo_form.save()
mo.action_confirm()
mo.action_assign()
self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, serial_number)
with Form(mo) as mo_form:
mo_form.qty_producing = 1
mo.move_raw_ids.move_line_ids.qty_done = 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()
# scrap the component
scrap = self.env['stock.scrap'].create({
'product_id': component.id,
'product_uom_id': component.uom_id.id,
'scrap_qty': 1,
'lot_id': serial_number.id,
})
scrap_location = scrap.scrap_location_id
scrap.do_scrap()
# unscrap the component
internal_move = self.env['stock.move'].create({
'name': component.name,
'location_id': scrap_location.id,
'location_dest_id': stock_location.id,
'product_id': component.id,
'product_uom': component.uom_id.id,
'product_uom_qty': 1.0,
'move_line_ids': [(0, 0, {
'product_id': component.id,
'location_id': scrap_location.id,
'location_dest_id': stock_location.id,
'product_uom_id': component.uom_id.id,
'qty_done': 1.0,
'lot_id': serial_number.id,
})],
})
internal_move._action_confirm()
internal_move._action_done()
# produce one with the unscrapped component
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_4
mo = mo_form.save()
mo.action_confirm()
mo.action_assign()
self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, serial_number)
with Form(mo) as mo_form:
mo_form.qty_producing = 1
mo.move_raw_ids.move_line_ids.qty_done = 1
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},
])
def test_generate_serial_button(self):
"""Test if lot in form "00000dd" is manually created, the generate serial
button can skip it and create the next one.
"""
mo, _bom, p_final, _p1, _p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='lot')
# generate lot lot_0 on MO
mo.action_generate_serial()
lot_0 = mo.lot_producing_id.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
self.assertEqual(lot_2, str(int(lot_1) + 1).zfill(7))
def test_assign_stock_move_date_on_mark_done(self):
product_final = self.env['product.product'].create({
'name': 'Finished Product',
'type': 'product',
})
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)
})
production.action_confirm()
production.qty_producing = 1
production.button_mark_done()
self.assertEqual(production.move_finished_ids.date, datetime(2024, 1, 15), "Stock move should be availbale after the production is done.")
def test_use_lot_already_consumed(self):
"""
Tracked-by-sn product
Produce SN
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',
'tracking': 'serial',
})
sn_lot01, sn_lot02 = self.env['stock.lot'].create([{
'product_id': component.id,
'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)
mo = self.env['mrp.production'].create({
'product_id': component.id,
'product_qty': 1,
'product_uom_id': component.uom_id.id,
'company_id': self.env.company.id,
})
mo.action_confirm()
mo.qty_producing = 1
mo.lot_producing_id = 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'},
])
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_4
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.lot_id = sn_lot01
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'},
])
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_4
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.lot_id = sn_lot01
with self.assertRaises(UserError):
mo.button_mark_done()
def test_produce_consume_unbuild_and_consume(self):
"""
(1) Produce SN
(2) Consume SN
Unbuild (2)
Consume SN
-> We should not raise any UserError
"""
component = self.bom_4.bom_line_ids.product_id
component.write({
'type': 'product',
'tracking': 'serial',
})
sn = self.env['stock.lot'].create({
'product_id': component.id,
'name': "SN",
'company_id': self.env.company.id,
})
mo_produce_sn = self.env['mrp.production'].create({
'product_id': component.id,
'product_qty': 1,
'product_uom_id': component.uom_id.id,
'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.button_mark_done()
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.lot_id = sn
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'},
])
unbuild_form = Form(self.env['mrp.unbuild'])
unbuild_form.mo_id = mo_consume_sn
unbuild_form.save().action_unbuild()
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.lot_id = sn
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'},
])
def test_produce_consume_unbuild_all_and_consume(self):
"""
(1) Produce SN
(2) Consume SN
Unbuild (2)
Unbuild (1)
Update stock with 1 SN
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',
'tracking': 'serial',
})
sn = self.env['stock.lot'].create({
'product_id': component.id,
'name': "SN",
'company_id': self.env.company.id,
})
mo_produce_sn = self.env['mrp.production'].create({
'product_id': component.id,
'product_qty': 1,
'product_uom_id': component.uom_id.id,
'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.button_mark_done()
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.lot_id = sn
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'},
])
unbuild_form = Form(self.env['mrp.unbuild'])
unbuild_form.mo_id = mo_consume_sn
unbuild_form.save().action_unbuild()
unbuild_form = Form(self.env['mrp.unbuild'])
unbuild_form.mo_id = mo_produce_sn
unbuild_form.save().action_unbuild()
self.env['stock.quant']._update_available_quantity(component, 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.lot_id = sn
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'},
])

View file

@ -0,0 +1,982 @@
# -*- 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
from odoo.exceptions import UserError
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)]
})
def test_unbuild_standart(self):
""" This test creates a MO and then creates 3 unbuild
orders for the final product. None of the products for this
test are tracked. 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')
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.product_qty = 3
x.save().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), 92, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.product_qty = 2
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.product_qty = 5
x.save().action_unbuild()
# Check quantity in stock after last unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
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_final_lot(self):
""" This test creates a MO and then creates 3 unbuild
orders for the final product. Only the final product is tracked
by lot. It checks the stock state after each order
and ensure it is correct.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot')
self.assertEqual(len(mo), 1, 'MO should have been created')
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)
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_form.lot_producing_id = lot
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, lot_id=lot), 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')
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
# This should fail since we do not choose a lot to unbuild for final product.
with self.assertRaises(AssertionError):
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.product_qty = 3
unbuild_order = x.save()
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.product_qty = 3
x.lot_id = lot
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 2, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.product_qty = 2
x.lot_id = lot
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 0, 'You should have 0 finalproduct in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.product_qty = 5
x.lot_id = lot
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
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):
""" 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
order and ensure it is correct.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot')
self.assertEqual(len(mo), 1, 'MO should have been created')
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)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
for ml in mo.move_raw_ids.mapped('move_line_ids'):
if ml.product_id.tracking != 'none':
self.assertEqual(ml.lot_id, lot, 'Wrong reserved lot.')
# FIXME sle: behavior change
mo_form = Form(mo)
mo_form.qty_producing = 5.0
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.lot_id = lot
ml.qty_done = 20
details_operation_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, lot_id=lot), 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')
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
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)
with self.assertRaises(UserError):
unbuild_order.action_unbuild()
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.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, lot_id=lot), 92, 'You should have 92 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.product_qty = 2
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.product_qty = 5
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 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_everything_tracked(self):
""" This test creates a MO and then creates 3 unbuild
orders for the final product. All the products for this
test are tracked. It checks the stock state after each order
and ensure it is correct.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_2='lot', tracking_base_1='lot')
self.assertEqual(len(mo), 1, 'MO should have been created')
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)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2)
mo.action_assign()
# FIXME sle: behavior change
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.lot_producing_id = 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
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
details_operation_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, lot_id=lot_final), 5, 'You should have the 5 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 80, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 5 product in stock')
# ---------------------------------------------------
# unbuild
# ---------------------------------------------------
x = Form(self.env['mrp.unbuild'])
with self.assertRaises(AssertionError):
x.product_id = p_final
x.bom_id = bom
x.product_qty = 3
x.save()
with self.assertRaises(AssertionError):
x.product_id = p_final
x.bom_id = bom
x.product_qty = 3
x.save()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.product_qty = 3
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 2, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 92 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.product_qty = 2
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 0, 'You should have 0 finalproduct in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 5, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.product_qty = 5
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 10, 'You should have consumed all the 5 product in stock')
def test_unbuild_with_duplicate_move(self):
""" This test creates a MO from 3 different lot on a consumed product (p2).
The unbuild order should revert the correct quantity for each specific lot.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='none', tracking_base_2='lot', tracking_base_1='none')
self.assertEqual(len(mo), 1, 'MO should have been created')
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)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_2)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot_3)
mo.action_assign()
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.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, lot_id=lot_1), 0, 'You should have consumed all the 1 product for lot 1 in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 3 product for lot 2 in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 1, 'You should have consumed only 1 product for lot3 in stock')
x = Form(self.env['mrp.unbuild'])
x.product_id = p_final
x.bom_id = bom
x.mo_id = mo
x.product_qty = 5
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have no more final product in stock after unbuild')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 1, 'You should have get your product with lot 1 in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have the 3 basic product for lot 2 in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 2, 'You should have get one product back for lot 3')
def test_production_links_with_non_tracked_lots(self):
""" This test produces an MO in two times and checks that the move lines are linked in a correct way
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_1='none', tracking_base_2='lot')
# Young Tom
# \ Botox - 4 - p1
# \ Old Tom - 1 - p2
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 = mo_form.save()
self.assertEqual(mo.move_raw_ids[1].quantity_done, 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
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()
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]
# 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 = 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
ml.lot_id = lot_2
details_operation_form.save()
action = mo.button_mark_done()
mo1 = mo.procurement_group_id.mrp_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')
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')
def test_unbuild_with_routes(self):
""" This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock
is created with Warehouse -> True.
The unbuild order should revert the consumed components into QC/Unbuild location for quality check
and then a picking should be generated for transferring components from QC/Unbuild location to stock.
"""
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
})
# 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, {
'name': 'Send Matrial QC/Unbuild -> Stock',
'action': 'push',
'picking_type_id': self.ref('stock.picking_type_internal'),
'location_src_id': unbuild_location.id,
'location_dest_id': self.stock_location.id,
})],
})
# Create a stockable product and its components
finshed_product = ProductObj.create({
'name': 'Table',
'type': 'product',
})
component1 = ProductObj.create({
'name': 'Table head',
'type': 'product',
})
component2 = ProductObj.create({
'name': 'Table stand',
'type': 'product',
})
# Create bom and add components
bom = self.env['mrp.bom'].create({
'product_id': finshed_product.id,
'product_tmpl_id': finshed_product.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'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})
]})
# Set on hand quantity
StockQuant._update_available_quantity(component1, self.stock_location, 1)
StockQuant._update_available_quantity(component2, self.stock_location, 1)
# Create mo
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finshed_product
mo_form.bom_id = bom
mo_form.product_uom_id = finshed_product.uom_id
mo_form.product_qty = 1.0
mo = mo_form.save()
self.assertEqual(len(mo), 1, 'MO should have been created')
mo.action_confirm()
mo.action_assign()
# Produce the final product
mo_form = Form(mo)
mo_form.qty_producing = 1.0
produce_wizard = 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(StockQuant._get_available_quantity(finshed_product, self.stock_location), 1, 'Table should be available in stock')
self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 0, 'Table head should not be available in stock')
self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 0, 'Table stand should not be available in stock')
# ---------------------------------------------------
# Unbuild
# ---------------------------------------------------
# Create an unbuild order of the finished product and set the destination loacation = QC/Unbuild
x = Form(self.env['mrp.unbuild'])
x.product_id = finshed_product
x.bom_id = bom
x.mo_id = mo
x.product_qty = 1
x.location_id = self.stock_location
x.location_dest_id = unbuild_location
x.save().action_unbuild()
# Check the available quantity of components and final product in stock
self.assertEqual(StockQuant._get_available_quantity(finshed_product, self.stock_location), 0, 'Table should not be available in stock as it is unbuild')
self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 0, 'Table head should not be available in stock as it is in QC/Unbuild location')
self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 0, 'Table stand should not be available in stock as it is in QC/Unbuild location')
# Find new generated picking
picking = self.env['stock.picking'].search([('product_id', 'in', [component1.id, component2.id])])
self.assertEqual(picking.location_id.id, unbuild_location.id, 'Wrong source location in picking')
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
picking._action_done()
# Check the available quantity of components and final product in stock
self.assertEqual(StockQuant._get_available_quantity(finshed_product, self.stock_location), 0, 'Table should not be available in stock')
self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 1, 'Table head should be available in stock as the picking is transferred')
self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 1, 'Table stand should be available in stock as the picking is transferred')
def test_unbuild_decimal_qty(self):
"""
Use case:
- 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.bom_1.product_qty = 3
self.bom_1.bom_line_ids.product_qty = 5
self.env['stock.quant']._update_available_quantity(self.product_2, self.stock_location, 3)
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.bom_1.product_id
mo_form.bom_id = self.bom_1
mo = mo_form.save()
mo.action_confirm()
mo.action_assign()
mo_form = Form(mo)
mo_form.qty_producing = 3
mo_form.save()
mo.button_mark_done()
uo_form = Form(self.env['mrp.unbuild'])
uo_form.mo_id = mo
# Unbuilding one product means a decimal quantity equal to 1 / 3 * 5 for each component
uo_form.product_qty = 1
uo = uo_form.save()
uo.action_unbuild()
self.assertEqual(uo.state, 'done')
def test_unbuild_similar_tracked_components(self):
"""
Suppose a MO with, in the components, two lines for the same tracked-by-usn product
When unbuilding such an MO, all SN used in the MO should be back in stock
"""
compo, finished = self.env['product.product'].create([{
'name': 'compo',
'type': 'product',
'tracking': 'serial',
}, {
'name': 'finished',
'type': 'product',
}])
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)
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished
with mo_form.move_raw_ids.new() as line:
line.product_id = compo
line.product_uom_qty = 1
with mo_form.move_raw_ids.new() as line:
line.product_id = compo
line.product_uom_qty = 1
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
mo.action_assign()
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
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
details_operation_form.save()
mo.button_mark_done()
uo_form = Form(self.env['mrp.unbuild'])
uo_form.mo_id = mo
uo_form.product_qty = 1
uo = uo_form.save()
uo.action_unbuild()
self.assertEqual(uo.produce_line_ids.filtered(lambda sm: sm.product_id == compo).lot_ids, lot01 + lot02)
def test_unbuild_and_multilocations(self):
"""
Basic flow: produce p_final, transfer it to a sub-location and then
unbuild it. The test ensures that the source/destination locations of an
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)]})
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]
mo, _, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=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()
mo_form = Form(mo)
mo_form.qty_producing = 1.0
mo = mo_form.save()
mo.button_mark_done()
# Transfer the finished product from WH/Stock to `subloc01`
internal_form = Form(self.env['stock.picking'])
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:
move.product_id = p_final
move.product_uom_qty = 1.0
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'])
unbuild_order_form.mo_id = mo
unbuild_order_form.location_id = subloc01
unbuild_order_form.location_dest_id = subloc02
unbuild_order = unbuild_order_form.save()
unbuild_order.action_unbuild()
self.assertRecordValues(unbuild_order.produce_line_ids, [
# pylint: disable=bad-whitespace
{'product_id': p_final.id, 'location_id': subloc01.id, 'location_dest_id': prod_location.id},
{'product_id': p2.id, 'location_id': prod_location.id, 'location_dest_id': subloc02.id},
{'product_id': p1.id, 'location_id': prod_location.id, 'location_dest_id': subloc02.id},
])
def test_compute_product_uom_id(self):
order = self.env['mrp.unbuild'].create({
'product_id': self.product_4.id,
})
self.assertEqual(order.product_uom_id, self.product_4.uom_id)
def test_compute_location_id(self):
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)
def test_use_unbuilt_sn_in_mo(self):
"""
use an unbuilt serial number in manufacturing order:
produce a tracked product, unbuild it and then use it as a component with the same SN in a mo.
"""
product_1 = self.env['product.product'].create({
'name': 'Product tracked by sn',
'type': 'product',
'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',
})
bom_1 = self.env['mrp.bom'].create({
'product_id': product_1.id,
'product_tmpl_id': product_1.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': component.id, 'product_qty': 1}),
],
})
product_2 = self.env['product.product'].create({
'name': 'finished Product',
'type': 'product',
})
self.env['mrp.bom'].create({
'product_id': product_2.id,
'product_tmpl_id': product_2.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': product_1.id, 'product_qty': 1}),
],
})
# mo1
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_1
mo_form.bom_id = bom_1
mo_form.product_qty = 1.0
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 1.0
mo_form.lot_producing_id = product_1_sn
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
#unbuild order
unbuild_form = Form(self.env['mrp.unbuild'])
unbuild_form.mo_id = mo
unbuild_form.save().action_unbuild()
#mo2
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_2
mo2 = mo_form.save()
mo2.action_confirm()
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
details_operation_form.save()
mo_form = Form(mo2)
mo_form.qty_producing = 1
mo2 = mo_form.save()
mo2.button_mark_done()
self.assertEqual(mo2.state, 'done', "Production order should be in done state.")
def test_unbuild_mo_with_tracked_product_and_component(self):
"""
Test that the unbuild order is correctly created when the finished product
and the component is tracked by serial number
"""
finished_product = self.env['product.product'].create({
'name': 'Product tracked by sn',
'type': 'product',
'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',
})
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_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': component.id, 'product_qty': 1}),
],
})
# mo_1
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.bom_id = bom_1
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.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()
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)
self.assertEqual(mo.unbuild_ids.produce_line_ids[1].lot_ids.id, False)
# set the component as tracked
component.tracking = 'serial'
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
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.bom_id = bom_1
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.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()
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)
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[1].lot_ids, component_sn)
def test_unbuild_different_qty(self):
"""
Test that the quantity to unbuild is the qty produced in the MO
BoM:
- 4x final product
components:
- 2 x (storable)
- 4 x (consumable)
- Create a MO with 4 final products to produce.
- Confirm and validate, then unlock the mo and update the qty produced to 10
- open the wizard to unbuild > the quantity proposed should be 10
- unbuild 4 units
- the move lines should be created with the correct quantity
"""
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_1
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.qty_producing = 4
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# unlock and update the qty produced
mo.action_toggle_is_locked()
with Form(mo) as mo_form:
mo_form.qty_producing = 10
self.assertEqual(mo.qty_producing, 10)
#unbuild order
unbuild_form = Form(self.env['mrp.unbuild'])
unbuild_form.mo_id = mo
# check that the quantity to unbuild is the qty produced in the MO
self.assertEqual(unbuild_form.product_qty, 10)
unbuild_form.product_qty = 3
unbuild_order = unbuild_form.save()
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},
])
def test_unbuild_update_forecasted_qty(self):
"""
Test that the unbuild correctly updates the forecasted quantity of a product.
"""
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
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product
mo_form.bom_id = bom
mo_form.product_qty = 20.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
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}])

View file

@ -0,0 +1,697 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form, tagged
from odoo.addons.mrp.tests.common import TestMrpCommon
@tagged('post_install', '-at_install')
class TestMultistepManufacturingWarehouse(TestMrpCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Required for `uom_id` to be visible in the view
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
# Required for `manufacture_steps` to be visible in the view
cls.env.user.groups_id += cls.env.ref('stock.group_adv_location')
# Create warehouse
cls.customer_location = cls.env['ir.model.data']._xmlid_to_res_id('stock.stock_location_customers')
warehouse_form = Form(cls.env['stock.warehouse'])
warehouse_form.name = 'Test Warehouse'
warehouse_form.code = 'TWH'
cls.warehouse = warehouse_form.save()
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
# Create manufactured product
product_form = Form(cls.env['product.product'])
product_form.name = 'Stick'
product_form.uom_id = cls.uom_unit
product_form.uom_po_id = cls.uom_unit
product_form.detailed_type = 'product'
product_form.route_ids.clear()
product_form.route_ids.add(cls.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(cls.warehouse.mto_pull_id.route_id)
cls.finished_product = product_form.save()
# Create raw product for manufactured product
product_form = Form(cls.env['product.product'])
product_form.name = 'Raw Stick'
product_form.detailed_type = 'product'
product_form.uom_id = cls.uom_unit
product_form.uom_po_id = cls.uom_unit
cls.raw_product = product_form.save()
# Create bom for manufactured product
bom_product_form = Form(cls.env['mrp.bom'])
bom_product_form.product_id = cls.finished_product
bom_product_form.product_tmpl_id = cls.finished_product.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'normal'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = cls.raw_product
bom_line.product_qty = 2.0
cls.bom = bom_product_form.save()
def _check_location_and_routes(self):
# Check manufacturing pull rule.
self.assertTrue(self.warehouse.manufacture_pull_id)
self.assertTrue(self.warehouse.manufacture_pull_id.active, self.warehouse.manufacture_to_resupply)
self.assertTrue(self.warehouse.manufacture_pull_id.route_id)
# Check new routes created or not.
self.assertTrue(self.warehouse.pbm_route_id)
# Check location should be created and linked to warehouse.
self.assertTrue(self.warehouse.pbm_loc_id)
self.assertEqual(self.warehouse.pbm_loc_id.active, self.warehouse.manufacture_steps != 'mrp_one_step', "Input location must be de-active for single step only.")
self.assertTrue(self.warehouse.manu_type_id.active)
def test_00_create_warehouse(self):
""" Warehouse testing for direct manufacturing """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'mrp_one_step'
self._check_location_and_routes()
# Check locations of existing pull rule
self.assertFalse(self.warehouse.pbm_route_id.rule_ids, 'only the update of global manufacture route should happen.')
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.lot_stock_id.id)
def test_01_warehouse_twostep_manufacturing(self):
""" Warehouse testing for picking before manufacturing """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm'
self._check_location_and_routes()
self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 2)
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.lot_stock_id.id)
def test_02_warehouse_twostep_manufacturing(self):
""" Warehouse testing for picking ans store after manufacturing """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
self._check_location_and_routes()
self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 3)
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.sam_loc_id.id)
def test_manufacturing_3_steps(self):
""" Test MO/picking before manufacturing/picking after manufacturing
components and move_orig/move_dest. Ensure that everything is created
correctly.
"""
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.finished_product
production_form.picking_type_id = self.warehouse.manu_type_id
production = production_form.save()
production.action_confirm()
move_raw_ids = production.move_raw_ids
self.assertEqual(len(move_raw_ids), 1)
self.assertEqual(move_raw_ids.product_id, self.raw_product)
self.assertEqual(move_raw_ids.picking_type_id, self.warehouse.manu_type_id)
pbm_move = move_raw_ids.move_orig_ids
self.assertEqual(len(pbm_move), 1)
self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
self.assertFalse(pbm_move.move_orig_ids)
move_finished_ids = production.move_finished_ids
self.assertEqual(len(move_finished_ids), 1)
self.assertEqual(move_finished_ids.product_id, self.finished_product)
self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
sam_move = move_finished_ids.move_dest_ids
self.assertEqual(len(sam_move), 1)
self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
self.assertFalse(sam_move.move_dest_ids)
def test_manufacturing_flow(self):
""" Simulate a pick pack ship delivery combined with a picking before
manufacturing and store after manufacturing. Also ensure that the MO and
the moves to stock are created with the generic pull rules.
In order to trigger the rule we create a picking to the customer with
the 'make to order' procure method
"""
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
warehouse.delivery_steps = 'pick_pack_ship'
self.warehouse.flush_model()
self.env.ref('stock.route_warehouse0_mto').active = True
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
picking_customer = self.env['stock.picking'].create({
'location_id': self.warehouse.wh_output_stock_loc_id.id,
'location_dest_id': self.customer_location,
'partner_id': self.env['ir.model.data']._xmlid_to_res_id('base.res_partner_4'),
'picking_type_id': self.warehouse.out_type_id.id,
})
self.env['stock.move'].create({
'name': self.finished_product.name,
'product_id': self.finished_product.id,
'product_uom_qty': 2,
'product_uom': self.uom_unit.id,
'picking_id': picking_customer.id,
'location_id': self.warehouse.wh_output_stock_loc_id.id,
'location_dest_id': self.customer_location,
'procure_method': 'make_to_order',
'origin': 'SOURCEDOCUMENT',
'state': 'draft',
})
picking_customer.action_confirm()
production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
self.assertTrue(production_order)
self.assertEqual(production_order.origin, 'SOURCEDOCUMENT', 'The MO origin should be the SO name')
self.assertNotEqual(production_order.name, 'SOURCEDOCUMENT', 'The MO name should not be the origin of the move')
picking_stock_preprod = self.env['stock.move'].search([
('product_id', '=', self.raw_product.id),
('location_id', '=', self.warehouse.lot_stock_id.id),
('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
('picking_type_id', '=', self.warehouse.pbm_type_id.id)
]).picking_id
picking_stock_postprod = self.env['stock.move'].search([
('product_id', '=', self.finished_product.id),
('location_id', '=', self.warehouse.sam_loc_id.id),
('location_dest_id', '=', self.warehouse.lot_stock_id.id),
('picking_type_id', '=', self.warehouse.sam_type_id.id)
]).picking_id
self.assertTrue(picking_stock_preprod)
self.assertTrue(picking_stock_postprod)
self.assertEqual(picking_stock_preprod.state, 'assigned')
self.assertEqual(picking_stock_postprod.state, 'waiting')
self.assertEqual(picking_stock_preprod.origin, production_order.name, 'The pre-prod origin should be the MO name')
self.assertEqual(picking_stock_postprod.origin, 'SOURCEDOCUMENT', 'The post-prod origin should be the SO name')
picking_stock_preprod.action_assign()
picking_stock_preprod.move_line_ids.qty_done = 4
picking_stock_preprod._action_done()
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.lot_stock_id).mapped('quantity')))
self.assertTrue(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id))
production_order.action_assign()
self.assertEqual(production_order.reservation_state, 'assigned')
self.assertEqual(picking_stock_postprod.state, 'waiting')
produce_form = Form(production_order)
produce_form.qty_producing = production_order.product_qty
production_order = produce_form.save()
production_order.button_mark_done()
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id).mapped('quantity')))
self.assertEqual(picking_stock_postprod.state, 'assigned')
picking_stock_pick = self.env['stock.move'].search([
('product_id', '=', self.finished_product.id),
('location_id', '=', self.warehouse.lot_stock_id.id),
('location_dest_id', '=', self.warehouse.wh_pack_stock_loc_id.id),
('picking_type_id', '=', self.warehouse.pick_type_id.id)
]).picking_id
self.assertEqual(picking_stock_pick.move_ids.move_orig_ids.picking_id, picking_stock_postprod)
def test_cancel_propagation(self):
""" Test cancelling moves in a 'picking before
manufacturing' and 'store after manufacturing' process. The propagation of
cancel depends on the default values on each rule of the chain.
"""
self.warehouse.manufacture_steps = 'pbm_sam'
self.warehouse.flush_model()
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
picking_customer = self.env['stock.picking'].create({
'location_id': self.warehouse.lot_stock_id.id,
'location_dest_id': self.customer_location,
'partner_id': self.env['ir.model.data']._xmlid_to_res_id('base.res_partner_4'),
'picking_type_id': self.warehouse.out_type_id.id,
})
self.env['stock.move'].create({
'name': self.finished_product.name,
'product_id': self.finished_product.id,
'product_uom_qty': 2,
'picking_id': picking_customer.id,
'product_uom': self.uom_unit.id,
'location_id': self.warehouse.lot_stock_id.id,
'location_dest_id': self.customer_location,
'procure_method': 'make_to_order',
})
picking_customer.action_confirm()
production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
self.assertTrue(production_order)
move_stock_preprod = self.env['stock.move'].search([
('product_id', '=', self.raw_product.id),
('location_id', '=', self.warehouse.lot_stock_id.id),
('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
('picking_type_id', '=', self.warehouse.pbm_type_id.id)
])
move_stock_postprod = self.env['stock.move'].search([
('product_id', '=', self.finished_product.id),
('location_id', '=', self.warehouse.sam_loc_id.id),
('location_dest_id', '=', self.warehouse.lot_stock_id.id),
('picking_type_id', '=', self.warehouse.sam_type_id.id)
])
self.assertTrue(move_stock_preprod)
self.assertTrue(move_stock_postprod)
self.assertEqual(move_stock_preprod.state, 'assigned')
self.assertEqual(move_stock_postprod.state, 'waiting')
move_stock_preprod._action_cancel()
self.assertEqual(production_order.state, 'confirmed')
production_order.action_cancel()
self.assertTrue(move_stock_postprod.state, 'cancel')
def test_no_initial_demand(self):
""" Test MO/picking before manufacturing/picking after manufacturing
components and move_orig/move_dest. Ensure that everything is created
correctly.
"""
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.finished_product
production_form.picking_type_id = self.warehouse.manu_type_id
production = production_form.save()
production.move_raw_ids.product_uom_qty = 0
production.action_confirm()
production.action_assign()
self.assertFalse(production.move_raw_ids.move_orig_ids)
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reservation_state, 'assigned')
def test_manufacturing_3_steps_flexible(self):
""" Test MO/picking before manufacturing/picking after manufacturing
components and move_orig/move_dest. Ensure that additional moves are put
in picking before manufacturing too.
"""
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
bom = self.env['mrp.bom'].search([
('product_id', '=', self.finished_product.id)
])
new_product = self.env['product.product'].create({
'name': 'New product',
'type': 'product',
})
bom.consumption = 'flexible'
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.finished_product
production_form.picking_type_id = self.warehouse.manu_type_id
production = production_form.save()
production.action_confirm()
production.is_locked = False
production_form = Form(production)
with production_form.move_raw_ids.new() as move:
move.product_id = new_product
move.product_uom_qty = 2
production = production_form.save()
move_raw_ids = production.move_raw_ids
self.assertEqual(len(move_raw_ids), 2)
pbm_move = move_raw_ids.move_orig_ids
self.assertEqual(len(pbm_move), 2)
self.assertTrue(new_product in pbm_move.product_id)
def test_3_steps_and_byproduct(self):
""" Suppose a warehouse with Manufacture option set to '3 setps' and a product P01 with a reordering rule.
Suppose P01 has a BoM and this BoM mentions that when some P01 are produced, some P02 are produced too.
This test ensures that when a MO is generated thanks to the reordering rule, 2 pickings are also
generated:
- One to bring the components
- Another to return the P01 and P02 produced
"""
warehouse = self.warehouse
warehouse.manufacture_steps = 'pbm_sam'
warehouse_stock_location = warehouse.lot_stock_id
pre_production_location = warehouse.pbm_loc_id
post_production_location = warehouse.sam_loc_id
one_unit_uom = self.env.ref('uom.product_uom_unit')
[two_units_uom, four_units_uom] = self.env['uom.uom'].create([{
'name': 'x%s' % i,
'category_id': self.ref('uom.product_uom_categ_unit'),
'uom_type': 'bigger',
'factor_inv': i,
} for i in [2, 4]])
finished_product = self.env['product.product'].create({
'name': 'Super Product',
'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))],
'type': 'product',
})
secondary_product = self.env['product.product'].create({
'name': 'Secondary',
'type': 'product',
})
component = self.env['product.product'].create({
'name': 'Component',
'type': 'consu',
})
self.env['mrp.bom'].create({
'product_tmpl_id': finished_product.product_tmpl_id.id,
'product_qty': 1,
'product_uom_id': two_units_uom.id,
'bom_line_ids': [(0, 0, {
'product_id': component.id,
'product_qty': 1,
'product_uom_id': one_unit_uom.id,
})],
'byproduct_ids': [(0, 0, {
'product_id': secondary_product.id,
'product_qty': 1,
'product_uom_id': four_units_uom.id,
})],
})
self.env['stock.warehouse.orderpoint'].create({
'warehouse_id': warehouse.id,
'location_id': warehouse_stock_location.id,
'product_id': finished_product.id,
'product_min_qty': 2,
'product_max_qty': 2,
})
self.env['procurement.group'].run_scheduler()
mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
pickings = mo.picking_ids
self.assertEqual(len(pickings), 2)
preprod_picking = pickings[0] if pickings[0].location_id == warehouse_stock_location else pickings[1]
self.assertEqual(preprod_picking.location_id, warehouse_stock_location)
self.assertEqual(preprod_picking.location_dest_id, pre_production_location)
postprod_picking = pickings - preprod_picking
self.assertEqual(postprod_picking.location_id, post_production_location)
self.assertEqual(postprod_picking.location_dest_id, warehouse_stock_location)
byproduct_postprod_move = self.env['stock.move'].search([
('product_id', '=', secondary_product.id),
('location_id', '=', post_production_location.id),
('location_dest_id', '=', warehouse_stock_location.id),
])
self.assertEqual(byproduct_postprod_move.state, 'waiting')
self.assertEqual(byproduct_postprod_move.group_id.name, mo.name)
def test_manufacturing_3_steps_trigger_reordering_rules(self):
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
with Form(self.raw_product) as p:
p.route_ids.clear()
p.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
# Create an additional BoM for component
product_form = Form(self.env['product.product'])
product_form.name = 'Wood'
product_form.detailed_type = 'product'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.wood_product = product_form.save()
# Create bom for manufactured product
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.raw_product
bom_product_form.product_tmpl_id = self.raw_product.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'normal'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.wood_product
bom_line.product_qty = 1.0
bom_product_form.save()
self.env['stock.quant']._update_available_quantity(
self.finished_product, self.warehouse.lot_stock_id, -1.0)
rr_form = Form(self.env['stock.warehouse.orderpoint'])
rr_form.product_id = self.wood_product
rr_form.location_id = self.warehouse.lot_stock_id
rr_form.save()
rr_form = Form(self.env['stock.warehouse.orderpoint'])
rr_form.product_id = self.finished_product
rr_form.location_id = self.warehouse.lot_stock_id
rr_finish = rr_form.save()
rr_form = Form(self.env['stock.warehouse.orderpoint'])
rr_form.product_id = self.raw_product
rr_form.location_id = self.warehouse.lot_stock_id
rr_form.save()
self.env['procurement.group'].run_scheduler()
pickings_component = self.env['stock.picking'].search(
[('product_id', '=', self.wood_product.id)])
self.assertTrue(pickings_component)
self.assertTrue(rr_finish.name in pickings_component.origin)
def test_2_steps_and_additional_moves(self):
""" Suppose a 2-steps configuration. If a user adds a product to an existing draft MO and then
confirms it, the associated picking should includes this new product"""
self.warehouse.manufacture_steps = 'pbm'
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.bom.product_id
mo_form.picking_type_id = self.warehouse.manu_type_id
mo = mo_form.save()
component_move = mo.move_raw_ids[0]
mo.with_context(default_raw_material_production_id=mo.id).move_raw_ids = [
[0, 0, {
'location_id': component_move.location_id.id,
'location_dest_id': component_move.location_dest_id.id,
'picking_type_id': component_move.picking_type_id.id,
'product_id': self.product_2.id,
'name': self.product_2.display_name,
'product_uom_qty': 1,
'product_uom': self.product_2.uom_id.id,
'warehouse_id': component_move.warehouse_id.id,
'raw_material_production_id': mo.id,
}]
]
mo.action_confirm()
self.assertEqual(self.bom.bom_line_ids.product_id + self.product_2, mo.picking_ids.move_ids.product_id)
def test_manufacturing_complex_product_3_steps(self):
""" Test MO/picking after manufacturing a complex product which uses
manufactured components. Ensure that everything is created and picked
correctly.
"""
self.warehouse.mto_pull_id.route_id.active = True
# Creating complex product which trigger another manifacture
routes = self.warehouse.manufacture_pull_id.route_id + self.warehouse.mto_pull_id.route_id
self.complex_product = self.env['product.product'].create({
'name': 'Arrow',
'type': 'product',
'route_ids': [(6, 0, routes.ids)],
})
# Create raw product for manufactured product
self.raw_product_2 = self.env['product.product'].create({
'name': 'Raw Iron',
'type': 'product',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
})
self.finished_product.route_ids = [(6, 0, routes.ids)]
# Create bom for manufactured product
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.complex_product
bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id
with bom_product_form.bom_line_ids.new() as line:
line.product_id = self.finished_product
line.product_qty = 1.0
with bom_product_form.bom_line_ids.new() as line:
line.product_id = self.raw_product_2
line.product_qty = 1.0
self.complex_bom = bom_product_form.save()
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.complex_product
production_form.picking_type_id = self.warehouse.manu_type_id
production = production_form.save()
production.action_confirm()
move_raw_ids = production.move_raw_ids
self.assertEqual(len(move_raw_ids), 2)
sfp_move_raw_id, raw_move_raw_id = move_raw_ids
self.assertEqual(sfp_move_raw_id.product_id, self.finished_product)
self.assertEqual(raw_move_raw_id.product_id, self.raw_product_2)
for move_raw_id in move_raw_ids:
self.assertEqual(move_raw_id.picking_type_id, self.warehouse.manu_type_id)
pbm_move = move_raw_id.move_orig_ids
self.assertEqual(len(pbm_move), 1)
self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
# Check move locations
move_finished_ids = production.move_finished_ids
self.assertEqual(len(move_finished_ids), 1)
self.assertEqual(move_finished_ids.product_id, self.complex_product)
self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
sam_move = move_finished_ids.move_dest_ids
self.assertEqual(len(sam_move), 1)
self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
self.assertFalse(sam_move.move_dest_ids)
subproduction = self.env['mrp.production'].browse(production.id+1)
sfp_pickings = subproduction.picking_ids.sorted('id')
# SFP Production: 2 pickings, 1 group
self.assertEqual(len(sfp_pickings), 2)
self.assertEqual(sfp_pickings.mapped('group_id'), subproduction.procurement_group_id)
# Move Raw Stick - Stock -> Preprocessing
picking = sfp_pickings[0]
self.assertEqual(len(picking.move_ids), 1)
picking.move_ids[0].product_id = self.raw_product
# Move SFP - PostProcessing -> Stock
picking = sfp_pickings[1]
self.assertEqual(len(picking.move_ids), 1)
picking.move_ids[0].product_id = self.finished_product
# Main production 2 pickings, 1 group
pickings = production.picking_ids.sorted('id')
self.assertEqual(len(pickings), 2)
self.assertEqual(pickings.mapped('group_id'), production.procurement_group_id)
# Move 2 components Stock -> Preprocessing
picking = pickings[0]
self.assertEqual(len(picking.move_ids), 2)
picking.move_ids[0].product_id = self.finished_product
picking.move_ids[1].product_id = self.raw_product_2
# Move FP PostProcessing -> Stock
picking = pickings[1]
self.assertEqual(len(picking.move_ids), 1)
picking.product_id = self.complex_product
def test_child_parent_relationship_on_backorder_creation(self):
""" Test Child Mo and Source Mo in 2/3-step production for reorder
rules in backorder using order points with the help of run scheduler """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
rr_form = Form(self.env['stock.warehouse.orderpoint'])
rr_form.product_id = self.finished_product
rr_form.product_min_qty = 20
rr_form.product_max_qty = 40
rr_form.save()
self.env['procurement.group'].run_scheduler()
mo = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
mo_form = Form(mo)
mo_form.qty_producing = 20
mo = mo_form.save()
action = mo.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
self.assertEqual(mo.mrp_production_child_count, 0, "Children MOs counted as existing where there should be none")
self.assertEqual(mo.mrp_production_source_count, 0, "Source MOs counted as existing where there should be none")
self.assertEqual(mo.mrp_production_backorder_count, 2)
def test_source_location_on_merge_mo_3_steps(self):
"""Check that default values are correct after merging mos when 3-step manufacturing"""
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
# picking with non default location
picking_type = self.env['stock.picking.type'].create({
'name': 'Manufacturing',
'code': 'mrp_operation',
'warehouse_id': warehouse.id,
'default_location_src_id': self.warehouse.pbm_loc_id.copy().id,
'default_location_dest_id': self.warehouse.sam_loc_id.copy().id,
'sequence_code': 'TMP',
'sequence_id': self.env['ir.sequence'].create({
'code': 'mrp.production',
'name': 'tmp_production_sequence',
}).id,
})
mo1_form = Form(self.env['mrp.production'])
mo1_form.product_id = self.finished_product
mo1_form.picking_type_id = picking_type
mo1 = mo1_form.save()
mo1.action_confirm()
mo2_form = Form(self.env['mrp.production'])
mo2_form.product_id = self.finished_product
mo2_form.picking_type_id = picking_type
mo2 = mo2_form.save()
mo2.action_confirm()
action = (mo1 + mo2).action_merge()
mo = self.env[action['res_model']].browse(action['res_id'])
self.assertEqual(picking_type.default_location_src_id, mo.move_raw_ids.location_id,
"The default source location of the merged mo should be the same as the 1st of the original MOs")
self.assertEqual(picking_type, mo.picking_type_id,
"The operation type of the merged mo should be the same as the 1st of the original MOs")
def test_manufacturing_bom_from_reordering_rules(self):
"""
Check that the manufacturing order is created with the BoM set in the reording rule:
- Create a product with 2 bill of materials,
- Create an orderpoint for this product specifying the 2nd BoM that must be used,
- Check that the MO has been created with the 2nd BoM
"""
manufacturing_route = self.env['stock.rule'].search([
('action', '=', 'manufacture')]).route_id
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
finished_product = self.env['product.product'].create({
'name': 'Product',
'type': 'product',
'route_ids': manufacturing_route,
})
self.env['mrp.bom'].create({
'product_tmpl_id': finished_product.product_tmpl_id.id,
'product_qty': 1,
'product_uom_id': finished_product.uom_id.id,
'type': 'normal',
})
bom_2 = self.env['mrp.bom'].create({
'product_tmpl_id': finished_product.product_tmpl_id.id,
'product_qty': 1,
'product_uom_id': finished_product.uom_id.id,
'type': 'normal',
})
self.env['stock.warehouse.orderpoint'].create({
'name': 'Orderpoint for P1',
'product_id': self.finished_product.id,
'product_min_qty': 1,
'product_max_qty': 1,
'route_id': manufacturing_route.id,
'bom_id': bom_2.id,
})
self.env['procurement.group'].run_scheduler()
mo = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
self.assertEqual(len(mo), 1)
self.assertEqual(mo.product_qty, 1.0)
self.assertEqual(mo.bom_id, bom_2)