19.0 vanilla

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

View file

@ -4,16 +4,19 @@ from . import test_bom
from . import test_byproduct
from . import test_cancel_mo
from . import test_order
from . import test_quant
from . import test_stock
from . import test_stock_report
from . import test_warehouse_multistep_manufacturing
from . import test_procurement
from . import test_replenish
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_consume_component
from . import test_manual_consumption
from . import test_workcenter
from . import test_mrp_reports

View file

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
from odoo import Command
from odoo.tests import Form
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.stock.tests import common2
from odoo.addons.stock.tests.common import TestStockCommon
class TestMrpCommon(common2.TestStockCommon):
class TestMrpCommon(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):
@ -14,21 +15,24 @@ class TestMrpCommon(common2.TestStockCommon):
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,
})
product_to_build, product_to_use_1, product_to_use_2 = cls.env['product.product'].create([
{
'name': 'Young Tom',
'type': 'consu',
'is_storable': True,
'tracking': tracking_final,
}, {
'name': 'Botox',
'type': 'consu',
'is_storable': True,
'tracking': tracking_base_1,
}, {
'name': 'Old Tom',
'type': 'consu',
'is_storable': True,
'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,
@ -37,9 +41,10 @@ class TestMrpCommon(common2.TestStockCommon):
'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})
]})
Command.create({'product_id': product_to_use_2.id, 'product_qty': qty_base_2}),
Command.create({'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:
@ -52,7 +57,26 @@ class TestMrpCommon(common2.TestStockCommon):
@classmethod
def setUpClass(cls):
super(TestMrpCommon, cls).setUpClass()
super().setUpClass()
cls.group_mrp_routings = cls.quick_ref('mrp.group_mrp_routings')
# Kept for reduced diff in existing tests, should be dropped someday
cls.product_7_template = cls.product_template_sofa
cls.product_7_attr1_v1 = cls.product_7_template.attribute_line_ids[
0].product_template_value_ids[0]
cls.product_7_attr1_v2 = cls.product_7_template.attribute_line_ids[
0].product_template_value_ids[1]
cls.product_7_attr1_v3 = cls.product_7_template.attribute_line_ids[
0].product_template_value_ids[2]
cls.product_7_1 = cls.product_7_template._get_variant_for_combination(
cls.product_7_attr1_v1)
cls.product_7_2 = cls.product_7_template._get_variant_for_combination(
cls.product_7_attr1_v2)
cls.product_7_3 = cls.product_7_template._get_variant_for_combination(
cls.product_7_attr1_v3)
(
cls.product_4,
@ -62,7 +86,6 @@ class TestMrpCommon(common2.TestStockCommon):
) = 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
}, {
@ -73,7 +96,8 @@ class TestMrpCommon(common2.TestStockCommon):
# 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',
'type': 'consu',
'is_storable': True,
})
# User Data: mrp user and mrp manager
@ -93,32 +117,41 @@ class TestMrpCommon(common2.TestStockCommon):
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,
})
# Both groups below are required to make fields `product_uom_id` and
# `workorder_ids` to be visible in the view of `mrp.production`. The
# field `product_uom_id` must be set by many tests, and subviews of
# `workorder_ids` must be present in many tests to create records.
cls.env.user.group_ids += cls.group_uom + cls.group_mrp_routings
cls.picking_type_manu = cls.warehouse_1.manu_type_id
cls.picking_type_manu.sequence = 5
cls.route_manufacture = cls.warehouse_1.manufacture_pull_id.route_id
cls.workcenter_1, cls.workcenter_2, cls.workcenter_3 = cls.env['mrp.workcenter'].create([
{
'name': 'Nuclear Workcenter',
'time_start': 10,
'time_stop': 5,
'time_efficiency': 80,
}, {
'name': 'Simple Workcenter',
'time_start': 0,
'time_stop': 0,
'time_efficiency': 100,
}, {
'name': 'Double Workcenter',
'time_start': 0,
'time_stop': 0,
'time_efficiency': 100,
}
])
for (workcenter, default_capacity) in [(cls.workcenter_1, 2), (cls.workcenter_2, 1), (cls.workcenter_3, 2)]:
cls.env['mrp.workcenter.capacity'].create({
'workcenter_id': workcenter.id,
'product_uom_id': cls.uom_unit.id,
'capacity': default_capacity,
'time_start': workcenter.time_start,
'time_stop': workcenter.time_stop,
})
cls.bom_1 = cls.env['mrp.bom'].create({
'product_id': cls.product_4.id,
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
@ -129,8 +162,8 @@ class TestMrpCommon(common2.TestStockCommon):
],
'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})
Command.create({'product_id': cls.product_2.id, 'product_qty': 2}),
Command.create({'product_id': cls.product_1.id, 'product_qty': 4}),
]})
cls.bom_2 = cls.env['mrp.bom'].create({
'product_id': cls.product_5.id,
@ -139,13 +172,13 @@ class TestMrpCommon(common2.TestStockCommon):
'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}),
Command.create({'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})
Command.create({'product_id': cls.product_4.id, 'product_qty': 2}),
Command.create({'product_id': cls.product_3.id, 'product_qty': 3}),
]})
cls.bom_3 = cls.env['mrp.bom'].create({
'product_id': cls.product_6.id,
@ -155,88 +188,65 @@ class TestMrpCommon(common2.TestStockCommon):
'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}),
Command.create({'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
Command.create({'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})
Command.create({'product_id': cls.product_5.id, 'product_qty': 2}),
Command.create({'product_id': cls.product_4.id, 'product_qty': 8}),
Command.create({'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}),
],
'operation_ids': [Command.create({
'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}),
Command.create({'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}),
],
'operation_ids': [Command.create({
'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}),
Command.create({'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}),
],
'operation_ids': [Command.create({
'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}),
Command.create({'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)
]
cls.stock_location_components = cls.shelf_1
@classmethod
def make_bom(cls, p, *cs):
@ -248,7 +258,7 @@ class TestMrpCommon(common2.TestStockCommon):
"type": "phantom",
"product_uom_id": cls.uom_unit.id,
"bom_line_ids": [
(0, 0, {
Command.create({
"product_id": c.id,
"product_qty": 1,
"product_uom_id": cls.uom_unit.id
@ -257,3 +267,17 @@ class TestMrpCommon(common2.TestStockCommon):
],
}
)
def full_availability(self):
"""set full availability for all calendars"""
calendar = self.env['resource.calendar'].search([])
calendar.write({'attendance_ids': [(5, 0, 0)]})
calendar.write({'attendance_ids': [
(0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
(0, 0, {'name': 'Saturday', 'dayofweek': '5', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
(0, 0, {'name': 'Sunday', 'dayofweek': '6', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
]})

View file

@ -1,239 +0,0 @@
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.")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import Form
from odoo.tests import common
from odoo.exceptions import ValidationError
@ -19,7 +20,7 @@ class TestMrpByProduct(common.TransactionCase):
def create_product(name, route_ids=[]):
return cls.env['product.product'].create({
'name': name,
'type': 'product',
'is_storable': True,
'route_ids': route_ids})
# Create product A, B, C.
@ -35,6 +36,19 @@ class TestMrpByProduct(common.TransactionCase):
'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})]
})
cls.produced_serial = cls.env['product.product'].create({
'name': 'Produced Serial',
'is_storable': True,
'tracking': 'serial',
})
cls.sn_1 = cls.env['stock.lot'].create({
'name': 'Serial_01',
'product_id': cls.produced_serial.id
})
cls.sn_2 = cls.env['stock.lot'].create({
'name': 'Serial_02',
'product_id': cls.produced_serial.id
})
def test_00_mrp_byproduct(self):
""" Test by product with production order."""
@ -105,7 +119,7 @@ class TestMrpByProduct(common.TransactionCase):
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.picked = True
mnf_product_a.move_raw_ids._action_done()
self.assertEqual(mnf_product_a.state, "progress")
mnf_product_a.qty_producing = 2
@ -169,9 +183,9 @@ class TestMrpByProduct(common.TransactionCase):
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,
- Only `move_finished_ids`(A + B) is passed, containing both the finished product and the by-products of the BOM,
- Both `move_finished_ids`(A + B) and `move_byproduct_ids`(B) are passed,
- Both `move_finished_ids`(A) and `move_byproduct_ids`(B) 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,
@ -215,8 +229,22 @@ class TestMrpByProduct(common.TransactionCase):
}),
],
}),
# Only `move_byproduct_ids` passed, containing the by-product move only
# Both `move_finished_ids`(A + B) and `move_byproduct_ids`(B) passed,
(2.0, 4.0, {
'move_finished_ids': [
(0, 0, {
'product_id': self.product_a.id,
'product_uom_qty': 2.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,
}),
],
'move_byproduct_ids': [
(0, 0, {
'product_id': self.product_b.id,
@ -226,7 +254,7 @@ class TestMrpByProduct(common.TransactionCase):
}),
],
}),
# Both `move_finished_ids` and `move_byproduct_ids` passed,
# Both `move_finished_ids`(A) and `move_byproduct_ids`(B) passed,
# containing respectively the finished product and the by-product
(3.0, 4.0, {
'move_finished_ids': [
@ -305,6 +333,7 @@ class TestMrpByProduct(common.TransactionCase):
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': stor_category.id,
'sublocation': 'closest_location',
})
self.env['stock.putaway.rule'].create({
'product_id': self.product_a.id,
@ -344,45 +373,39 @@ class TestMrpByProduct(common.TransactionCase):
# Create product
self.product_d = self.env['product.product'].create({
'name': 'Product D',
'type': 'product'})
'is_storable': True})
self.product_e = self.env['product.product'].create({
'name': 'Product E',
'type': 'product'})
'is_storable': True})
# 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():
with self.assertRaises(ValidationError):
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():
with self.assertRaises(ValidationError):
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():
with self.assertRaises(ValidationError):
byproduct_1.cost_share = 60
byproduct_2.cost_share = 70
mo.write({'move_byproduct_ids': [(6, 0, [byproduct_1.id, byproduct_2.id])]})
@ -404,18 +427,142 @@ class TestMrpByProduct(common.TransactionCase):
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_01_check_byproducts_update(self):
"""
Test that check byproducts update in stock move should also reflect in stock move line(Product moves).
"""
# Create new MO
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_a
mo_form.product_qty = 1.0
mo = mo_form.save()
mo.action_confirm()
mo.move_byproduct_ids.write({'product_id': self.product_c_id})
mo.button_mark_done()
self.assertEqual(mo.move_byproduct_ids.product_id, mo.move_byproduct_ids.move_line_ids.product_id)
def test_02_check_byproducts_update(self):
"""
Case 2: Update Product From Tracked Product to Non Tracked Product.
"""
self.bom_byproduct.byproduct_ids[0].product_id = self.produced_serial.id
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()
mo.move_byproduct_ids.lot_ids = [(4, self.sn_1.id)]
mo.move_byproduct_ids.lot_ids = [(4, self.sn_2.id)]
self.assertEqual(len(mo.move_byproduct_ids.move_line_ids), 2)
mo.move_byproduct_ids.write({'product_id': self.product_c_id})
mo.button_mark_done()
self.assertEqual(len(mo.move_byproduct_ids.move_line_ids), 1)
self.assertEqual(mo.move_byproduct_ids.product_id, mo.move_byproduct_ids.move_line_ids.product_id)
def test_03_check_byproducts_update(self):
"""
Case 3: Update Product From Non Tracked Product to Tracked Product.
"""
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_a
mo_form.product_qty = 2.0
mo = mo_form.save()
mo.action_confirm()
mo.move_byproduct_ids.write({'product_id': self.produced_serial.id})
mo.move_byproduct_ids.lot_ids = [(4, self.sn_1.id)]
mo.move_byproduct_ids.lot_ids = [(4, self.sn_2.id)]
self.assertFalse(mo.move_byproduct_ids.show_lots_text)
self.assertTrue(mo.move_byproduct_ids.show_lots_m2o)
self.assertFalse(mo.move_byproduct_ids.show_quant)
mo.button_mark_done()
self.assertEqual(len(mo.move_byproduct_ids.move_line_ids), 2)
self.assertEqual(mo.move_byproduct_ids.product_id, mo.move_byproduct_ids.move_line_ids.product_id)
def test_byproduct_qty_update(self):
"""
Test that byproduct quantity is updated to the quantity set on the Mo when the Mo is marked as done.ee
"""
self.bom_byproduct.byproduct_ids.product_qty = 0.0
self.warehouse.manufacture_steps = 'pbm_sam'
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()
mo.move_byproduct_ids.quantity = 1.0
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
picking = mo.picking_ids.filtered(lambda p: p.location_dest_id == self.warehouse.lot_stock_id)
self.assertEqual(picking.state, 'assigned')
byproduct_move = picking.move_ids.filtered(lambda m: m.product_id == self.bom_byproduct.byproduct_ids.product_id)
self.assertEqual(byproduct_move.product_qty, 1.0)
def test_byproducts_bom_document(self):
self.env.user.group_ids += self.env.ref('mrp.group_mrp_byproducts')
doc_product_bom = self.env['product.document'].create({
'name': 'doc_product_bom',
'attached_on_mrp': 'bom',
'res_id': self.product_a.id,
'res_model': 'product.product',
})
# ensures that the archived docs are not taken into account
self.env['product.document'].create({
'name': 'doc_product_bom_archived',
'active': False,
'attached_on_mrp': 'bom',
'res_id': self.product_a.id,
'res_model': 'product.product',
})
doc_template_bom = self.env['product.document'].create({
'name': 'doc_template_bom',
'attached_on_mrp': 'bom',
'res_id': self.product_a.product_tmpl_id.id,
'res_model': 'product.template',
})
attachments = doc_template_bom.ir_attachment_id + doc_product_bom.ir_attachment_id
bom = self.env['mrp.bom'].create({
'product_tmpl_id': self.product_b.product_tmpl_id.id,
'product_uom_id': self.product_b.product_tmpl_id.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'byproduct_ids': [
Command.create({
'product_id': self.product_a.id,
'product_qty': 1
}),
]
})
# the two docs linked to the byproduct should be in the chatter
self.assertEqual(bom._get_extra_attachments(), attachments)
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')
self.env.user.group_ids += self.env.ref('mrp.group_mrp_byproducts')
component, final_product, byproduct = self.env['product.product'].create([{
'name': name,
'type': 'product'
'is_storable': True,
} 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({
@ -425,18 +572,41 @@ class TestMrpByProduct(common.TransactionCase):
mo_form = Form(mo)
with mo_form.move_raw_ids.new() as line:
line.product_id = component
line.product_uom_qty = 1
with mo_form.move_byproduct_ids.new() as line:
line.product_id = byproduct
line.product_uom_qty = 1
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)
def test_over_produce_by_products_with_cost_share(self):
"""
Tests that overproducing by-products with a set cost share
behaves as expected (as it should rely on the merge move) for
the extra move.
"""
# Create new MO
self.env.user.group_ids = [Command.link(self.ref('mrp.group_mrp_byproducts'))]
self.bom_byproduct.byproduct_ids.cost_share = 3.3
mo = self.env['mrp.production'].create({
'product_id': self.product_a.id,
'product_qty': 1.0,
})
mo.action_confirm()
with Form(mo) as mo_form:
mo_form.qty_producing = 1.0
with mo_form.move_byproduct_ids.edit(0) as by_product_move:
by_product_move.quantity = 10.0
mo.button_mark_done()
self.assertRecordValues(mo.move_byproduct_ids, [
{'quantity': 10.0, 'state': 'done'},
])

View file

@ -116,3 +116,25 @@ class TestMrpCancelMO(TestMrpCommon):
self.assertEqual(mo.move_finished_ids.state, 'cancel')
self.assertEqual(mo.state, 'cancel')
def test_cannot_cancel_done_mo_with_three_steps(self):
"""Test that a done manufacturing order cannot be canceled.
The test ensures that when the warehouse uses a 3-step manufacturing route (Pick Produce Store),
attempting to cancel a manufacturing order that is already in 'done' state raises a UserError.
It also verifies that the linked pickings are not canceled in this case.
"""
# Enable 3-step manufacturing process
self.warehouse_1.manufacture_steps = 'pbm_sam'
# Create and confirm a manufacturing order
mo = self.env['mrp.production'].create({
'bom_id': self.bom_1.id,
})
mo.action_confirm()
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
with self.assertRaises(UserError):
mo.action_cancel()
self.assertNotEqual(mo.picking_ids.mapped('state'), ['cancel', 'cancel'])
with self.assertRaises(UserError):
mo.unlink()

View file

@ -0,0 +1,473 @@
import copy
from odoo.exceptions import UserError
from odoo.tests import common, tagged, Form
class TestConsumeComponentCommon(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
# Create Products & Components
cls.produced_lot = cls.env['product.product'].create({
'name': 'Produced Lot',
'is_storable': True,
'tracking': 'lot',
'route_ids': [(4, cls.manufacture_route.id, 0)],
})
cls.produced_serial = cls.env['product.product'].create({
'name': 'Produced Serial',
'is_storable': True,
'tracking': 'serial',
'route_ids': [(4, cls.manufacture_route.id, 0)],
})
cls.produced_none = cls.env['product.product'].create({
'name': 'Produced None',
'is_storable': True,
'tracking': 'none',
'route_ids': [(4, cls.manufacture_route.id, 0)],
})
cls.raw_lot = cls.env['product.product'].create({
'name': 'Raw Lot',
'is_storable': True,
'tracking': 'lot',
})
cls.raw_serial = cls.env['product.product'].create({
'name': 'Raw Serial',
'is_storable': True,
'tracking': 'serial',
})
cls.raw_none = cls.env['product.product'].create({
'name': 'Raw None',
'is_storable': True,
'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,
}).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))
mos = cls.env['mrp.production'].create(vals)
return mos
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)
isComponentTracking = any(move.has_tracking != 'none' 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_qty_producing()
i = 1
if isSerial:
mrp_productions[i].action_generate_serial()
i += 1
if isAvailable:
error = False
try:
mrp_productions[i].button_mark_done()
except UserError:
error = True
self.assertFalse(error, "Immediate Production shall not raise an error.")
@tagged('post_install', '-at_install')
class TestConsumeComponent(TestConsumeComponentCommon):
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.quantity, "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.assertTrue(mov.picked, "All components should be picked")
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.quantity, "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.assertTrue(mov.picked, "components should be picked even without no quantity reserved")
else:
self.assertEqual(mov.product_qty, mov.quantity, "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()
# 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.quantity, "Reserved quantity shall be equal to " + str(raw_none_qty) + ".")
else:
self.assertEqual(raw_tracked_qty, mov.quantity, "Reserved quantity shall be equal to " + str(raw_tracked_qty) + ".")
if serialTrigger is None:
self.executeConsumptionTriggers(mo)
elif serialTrigger == 1:
mo.qty_producing = 1
mo._set_qty_producing(False)
elif serialTrigger == 2:
mo.action_generate_serial()
for mov in mo.move_raw_ids:
if mov.has_tracking == "none":
self.assertTrue(mov.picked, "non tracked components should be picked")
else:
self.assertEqual(mov.product_qty, mov.quantity, "Done quantity shall be equal to To Consume quantity.")
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)
def test_tracked_production_2_steps_manufacturing(self):
"""
Create an MO for a product tracked by SN in 2-steps manufacturing with tracked components.
Assign a SN to the final product using the auto generation, then validate the pbm picking.
This test checks that the tracking of components is updated on the MO.
"""
warehouse = self.env.ref('stock.warehouse0')
warehouse.manufacture_steps = 'pbm'
bom = self.bom_serial
bom.product_id = self.produced_serial
components = self.bom_serial.bom_line_ids.mapped('product_id')
lot_1 = self.env['stock.lot'].create({
'name': 'lot_1',
'product_id': components[1].id,
'company_id': self.env.company.id,
})
lot_2 = self.env['stock.lot'].create({
'name': 'SN01',
'product_id': components[2].id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(components[0], self.env.ref('stock.warehouse0').lot_stock_id, 3)
self.env['stock.quant']._update_available_quantity(components[1], self.env.ref('stock.warehouse0').lot_stock_id, 2, lot_id=lot_1)
self.env['stock.quant']._update_available_quantity(components[2], self.env.ref('stock.warehouse0').lot_stock_id, 1, lot_id=lot_2)
mo = self.env['mrp.production'].create({
'product_id': bom.product_id.id,
'product_qty': 1,
'bom_id': bom.id,
})
mo.action_confirm()
self.assertRecordValues(mo.picking_ids.move_ids, [
{'quantity': 3.0, 'picked': False, 'lot_ids': []},
{'quantity': 2.0, 'picked': False, 'lot_ids': lot_1.ids},
{'quantity': 1.0, 'picked': False, 'lot_ids': lot_2.ids},
])
mo.action_generate_serial()
self.assertRecordValues(mo.move_raw_ids, [
{'should_consume_qty': 3.0, 'quantity': 3.0, 'picked': True, 'lot_ids': []},
{'should_consume_qty': 2.0, 'quantity': 0.0, 'picked': False, 'lot_ids': []},
{'should_consume_qty': 1.0, 'quantity': 0.0, 'picked': False, 'lot_ids': []},
])
self.assertTrue(mo.lot_producing_ids)
mo.picking_ids.button_validate()
self.assertRecordValues(mo.move_raw_ids, [
{'quantity': 3.0, 'picked': True, 'lot_ids': []},
{'quantity': 2.0, 'picked': False, 'lot_ids': lot_1.ids},
{'quantity': 1.0, 'picked': False, 'lot_ids': lot_2.ids},
])
mo.move_raw_ids.picked = True
mo.button_mark_done()
def test_automatic_consume_new_added_component(self):
"""
Create an MO for a product and set qty_producing than add a new component with quantity and automatically it's picked.
"""
sfg_product, compo1, compo2 = self.env['product.product'].create([
{
'name': 'SFG Product',
'is_storable': True,
'route_ids': [(4, self.manufacture_route.id, 0)],
},
{
'name': 'Compo 1',
'is_storable': True,
},
{
'name': 'Compo 2',
'is_storable': True,
}
])
quant = self.create_quant(compo1, 10)
quant |= self.create_quant(compo2, 10)
quant.action_apply_inventory()
bom = self.env['mrp.bom'].create({
'product_tmpl_id': sfg_product.product_tmpl_id.id,
'product_uom_id': sfg_product.uom_id.id,
'consumption': 'flexible',
'sequence': 1
})
self.create_bom_lines(bom, compo1, [1])
mo = self.env['mrp.production'].create({
'product_id': sfg_product.id,
'product_uom_id': sfg_product.uom_id.id,
'product_qty': 1,
'bom_id': bom.id
})
mo.action_confirm()
self.assertRecordValues(mo.move_raw_ids, [
{'product_uom_qty': 1.0, 'picked': False},
])
with Form(mo) as mo_form:
mo_form.qty_producing = 1.0
self.assertRecordValues(mo.move_raw_ids, [
{'should_consume_qty': 1.0, 'quantity': 1.0, 'picked': True},
])
move = self.env['stock.move'].create({
'product_id': compo2.id,
'raw_material_production_id': mo.id,
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.env.company.id)]).id,
})
move.should_consume_qty = 1
move.quantity = 1
move._action_assign()
self.assertRecordValues(mo.move_raw_ids, [
{'should_consume_qty': 1.0, 'quantity': 1.0, 'picked': True},
{'should_consume_qty': 1.0, 'quantity': 1.0, 'picked': True},
])
def test_no_component_consumption_on_lot_removal(self):
"""
If we have a manufacturing order (MO) for a product tracked by lot, and we assign a lot number
and then unassign it, the components should not be consumed at that point.
"""
quant = self.create_quant(self.raw_none, 3)
quant |= self.create_quant(self.raw_lot, 2)
quant |= self.create_quant(self.raw_serial, 1)
quant.action_apply_inventory()
mo = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_TRIGGERS_COUNT)
mo.action_confirm()
mo.action_generate_serial()
self.assertRecordValues(mo.move_raw_ids, [
{'quantity': 3.0, 'picked': False},
{'quantity': 2.0, 'picked': False},
{'quantity': 1.0, 'picked': False},
])
mo.action_clear_lot_producing_ids()
self.assertRecordValues(mo.move_raw_ids, [
{'quantity': 0.0, 'picked': False},
{'quantity': 0.0, 'picked': False},
{'quantity': 0.0, 'picked': False},
])

View file

@ -1,157 +0,0 @@
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

@ -1,37 +1,28 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# -*- coding: utf-8 -*-
from odoo import Command
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.
def test_mrp_manual_consumption_02(self):
"""
test that when a new quantity is manually set for a component,
and the field picked is set to True,
and the MO is marked as done, the component quantity is not overwritten.
"""
Product = self.env['product.product']
product_finish = Product.create({
'name': 'finish',
'type': 'product',
'is_storable': True,
'tracking': 'none',})
product_nt = Product.create({
'name': 'No tracking',
'type': 'product',
'is_storable': True,
'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,
@ -39,8 +30,6 @@ class TestTourManualConsumption(HttpCase):
'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}),
],
})
@ -50,141 +39,318 @@ class TestTourManualConsumption(HttpCase):
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(mo.state, 'confirmed')
move_nt = 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)
self.assertEqual(move_nt.quantity, 0)
self.assertFalse(move_nt.picked)
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)
url = f"/odoo/action-mrp.mrp_production_action/{mo.id}"
self.start_tour(url, "test_mrp_manual_consumption_02", login="admin", timeout=100)
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)
self.assertEqual(move_nt.picked, True)
self.assertEqual(move_nt.quantity, 16.0)
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,
cls.env.ref('base.group_user').write({
'implied_ids': [Command.link(cls.env.ref('stock.group_production_lot').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.
def test_manual_consumption_with_different_component_price(self):
"""
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,
Test that the moves are merged correctly, even if the products have been used with different prices:
- Create a product with a price of $10 and use it in a BoM with 1 unit.
- Create a MO with this BoM and confirm it.
- Update the price of the component to $20 and adjust the consumed quantity to 2.
- Mark the MO as done.
- Another move should be created and merged with the first move.
"""
self.bom_4.consumption = 'warning'
component = self.bom_4.bom_line_ids.product_id
component.write({
'is_storable': True,
'standard_price': 10,
})
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)
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 2)
mo = self.env['mrp.production'].create({
'product_qty': 1,
'bom_id': self.bom_4.id,
})
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
component.standard_price = 20
mo.move_raw_ids.quantity = 2.0
mo.move_raw_ids.picked = True
mo.move_raw_ids.manual_consumption = True
self.assertEqual(mo.state, 'progress')
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]
consumption_warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context']))
action = consumption_warning.save().action_confirm()
self.assertEqual(len(mo.move_raw_ids), 1)
self.assertEqual(mo.move_raw_ids.quantity, 2)
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.
def test_manual_consumption_quantity_change(self):
"""Test manual consumption mechanism.
1. Test when a move is manual consumption but NOT picked, quantity will be updated automatically.
2. Test when a move is manual consumption but IS picked, quantity will not be updated automatically.
3. Test when create backorder, the manual consumption should be set according to the bom.
"""
# 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)
Product = self.env['product.product']
product_finish = Product.create({
'name': 'finish',
'is_storable': True,
'tracking': 'none'})
product_auto_consumption = Product.create({
'name': 'Automatic',
'is_storable': True,
'tracking': 'none'})
product_manual_consumption = Product.create({
'name': 'Manual',
'is_storable': True,
'tracking': 'none'})
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_auto_consumption.id, 'product_qty': 1}),
(0, 0, {'product_id': product_manual_consumption.id, 'product_qty': 1}),
],
})
# 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)
def get_moves(mo):
move_auto = mo.move_raw_ids.filtered(lambda m: m.product_id == product_auto_consumption)
move_manual = mo.move_raw_ids.filtered(lambda m: m.product_id == product_manual_consumption)
return move_auto, move_manual
# 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)
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()
# After updating qty_producing, quantity changes for both moves, but manual move will remain not picked
mo_form = Form(mo)
mo_form.qty_producing = 5
mo = mo_form.save()
move_auto, move_manual = get_moves(mo)
self.assertEqual(move_auto.manual_consumption, False)
self.assertEqual(move_auto.quantity, 5)
self.assertTrue(move_auto.picked)
self.assertEqual(move_manual.manual_consumption, False)
self.assertEqual(move_manual.quantity, 5)
self.assertTrue(move_manual.picked)
move_manual.quantity = 6
move_manual._onchange_quantity()
# Now we change quantity to 7. Automatic move will change quantity, but manual move will still be 5 because it has been already picked.
mo_form = Form(mo)
mo_form.qty_producing = 7
mo = mo_form.save()
self.assertEqual(move_auto.quantity, 7)
self.assertEqual(move_manual.quantity, 6)
# Bypass consumption issues wizard and create backorders
action = mo.button_mark_done()
warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context']))
consumption = warning.save()
action = consumption.action_set_qty()
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder_form.save().action_backorder()
backorder = mo.production_group_id.production_ids - mo
# Check that backorders move have the same manual consumption values as BoM
move_auto, move_manual = get_moves(backorder)
self.assertEqual(move_auto.manual_consumption, False)
self.assertEqual(move_manual.manual_consumption, False)
def test_update_manual_consumption_00(self):
"""
Check that the manual consumption is set to true when the quantity is manualy set.
"""
bom = self.bom_1
components = bom.bom_line_ids.product_id
self.env['stock.quant']._update_available_quantity(components[0], self.stock_location, 10)
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = bom
mo_form.product_qty = 4
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.move_raw_ids.mapped('manual_consumption'), [False, False])
self.assertEqual(components[0].stock_quant_ids.reserved_quantity, 2.0)
with Form(mo) as fmo:
with fmo.move_raw_ids.edit(0) as line_0:
line_0.quantity = 3.0
line_0.picked = True
self.assertEqual(mo.move_raw_ids.mapped('manual_consumption'), [True, False])
self.assertEqual(components[0].stock_quant_ids.reserved_quantity, 3.0)
mo.button_mark_done()
self.assertRecordValues(mo.move_raw_ids, [{'quantity': 3.0, 'picked': True}, {'quantity': 4.0, 'picked': True}])
def test_update_manual_consumption_01(self):
"""
Check that the quantity of a raw line that is manually consumed is not updated
when the qty producing is changed and that others are.
"""
bom = self.bom_1
components = bom.bom_line_ids.product_id
self.env['stock.quant']._update_available_quantity(components[0], self.stock_location, 10)
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = bom
mo_form.product_qty = 4
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.move_raw_ids.mapped('manual_consumption'), [False, False])
self.assertEqual(components[0].stock_quant_ids.reserved_quantity, 2.0)
with Form(mo) as fmo:
with fmo.move_raw_ids.edit(0) as line_0:
line_0.quantity = 3.0
line_0.picked = True
fmo.qty_producing = 2.0
self.assertEqual(mo.move_raw_ids.mapped('manual_consumption'), [True, False])
self.assertEqual(components[0].stock_quant_ids.reserved_quantity, 3.0)
self.assertRecordValues(mo.move_raw_ids, [{'quantity': 3.0, 'picked': True}, {'quantity': 2.0, 'picked': True}])
def test_reservation_state_with_manual_consumption(self):
"""
Check that the reservation state of an MO is not influenced by moves without demand.
"""
self.warehouse_1.manufacture_steps = "pbm"
bom = self.bom_1
components = bom.bom_line_ids.mapped('product_id')
components.is_storable = True
# make the second component optional
bom.bom_line_ids[-1].product_qty = 0.0
self.env['stock.quant']._update_available_quantity(components[0], self.warehouse_1.lot_stock_id, 10.0)
mo_form = Form(self.env['mrp.production'])
mo_form.picking_type_id = self.picking_type_manu
mo_form.bom_id = bom
mo_form.product_qty = 4
mo = mo_form.save()
mo.action_confirm()
self.assertRecordValues(mo.picking_ids.move_ids, [
{ "product_id": components[0].id, "product_uom_qty": 2.0}
])
self.assertEqual(mo.reservation_state, "waiting")
mo.picking_ids.button_validate()
self.assertEqual(mo.reservation_state, "assigned")
mo.move_raw_ids.filtered(lambda m: m.product_id == components[0]).picked = True
self.assertEqual(mo.reservation_state, "assigned")
def test_no_consumption_when_quant_changed(self):
"""
Test to ensure that from 'Details' wizard, changing only the lot or location
of a component move line/quant (without changing the quantity) does not mark it as consumed.
The wizard (opened via 'action_show_details' on move) should only set
'manual_consumption' and 'picked' to True when the done quantity(quantity)
differs from the demanded quantity(product_uom_qty).
"""
bom = self.bom_4
component = bom.bom_line_ids.product_id
component.write({
"is_storable": True,
"tracking": "lot",
})
# Create two lots with quants.
lots = self.env["stock.lot"].create([
{"name": f"lot_{i}", "product_id": component.id} for i in range(2)
])
for lot in lots:
self.env["stock.quant"]._update_available_quantity(
component, self.stock_location, 5, lot_id=lot
)
# Create and confirm a Manufacturing Order.
mo_form = Form(self.env["mrp.production"])
mo_form.bom_id = bom
mo_form.product_qty = 1
mo = mo_form.save()
mo.action_confirm()
# Initially: not consumed.
self.assertRecordValues(mo.move_raw_ids, [
{"manual_consumption": False, "picked": False, "lot_ids": lots[0].ids},
])
# Change only the lot in the 'Details' wizard, keep quantity unchanged.
with Form.from_action(self.env, mo.move_raw_ids[0].action_show_details()) as wiz_form:
with wiz_form.move_line_ids.edit(0) as move_line:
move_line.lot_id = lots[1]
wiz_form.save()
# Still it should not consumed.
self.assertRecordValues(mo.move_raw_ids, [
{"manual_consumption": False, "picked": False, "lot_ids": lots[1].ids},
])
# Change the quantity in the 'Details' wizard.
with Form.from_action(self.env, mo.move_raw_ids[0].action_show_details()) as wiz_form:
with wiz_form.move_line_ids.edit(0) as move_line:
move_line.quantity = 2
wiz_form.save()
# Now it should be marked as consumed, since the done quantity differs from the demand.
self.assertRecordValues(mo.move_raw_ids, [
{"manual_consumption": True, "picked": True, "lot_ids": lots[1].ids},
])
def test_manual_consumption_is_false_if_quantity_was_unchanged(self):
"""
Check that a move's `manual_consumption` field is only set if the
quantity of the move line was modified.
"""
product_lot = self.env['product.product'].create({
'name': 'Product Lot',
'is_storable': True,
'tracking': 'lot',
})
lot_1, lot_2 = self.env['stock.lot'].create([{
'name': 'lot_1', 'product_id': product_lot.id,
}, {
'name': 'lot_2', 'product_id': product_lot.id,
}])
self.env['stock.quant']._update_available_quantity(product_lot, self.stock_location, 2, lot_id=lot_1)
self.env['stock.quant']._update_available_quantity(product_lot, self.stock_location, 2, lot_id=lot_2)
# Create an MO with one component from lot_1, quantity 2
mo = self.env['mrp.production'].create({
'product_id': self.product.id,
'product_qty': 2,
'move_raw_ids': [Command.create({
'product_id': product_lot.id,
'quantity': 2,
'move_line_ids': [Command.create({
'product_id': product_lot.id,
'lot_id': lot_1.id,
})],
})],
})
# Using the details form, change only the lot of the move line
action = mo.move_raw_ids.action_show_details()
details_form = Form(mo.move_raw_ids.with_context(action['context']), view=action['view_id'])
with details_form.move_line_ids.edit(0) as move_line:
move_line.lot_id = lot_2
move = details_form.save()
# Since quantity was unchanged, `manual_consumption` should not be set
self.assertFalse(move.manual_consumption)
# Use the form again, this time changing the lot and the quantity
with details_form.move_line_ids.edit(0) as move_line:
move_line.lot_id = lot_1
move_line.quantity = 1
move = details_form.save()
# Quantity was modified, so `manual_consumption` should be set
self.assertTrue(move.manual_consumption)

View file

@ -0,0 +1,41 @@
from odoo.tests import HttpCase, tagged
from odoo.fields import Command
@tagged('post_install', '-at_install')
class TestReportBom(HttpCase):
def test_mrp_report_bom_variant_selection(self):
self.env.ref('base.user_admin').write({'group_ids': [
Command.link(self.env.ref('product.group_product_variant').id),
]})
attribute = self.env['product.attribute'].create({'name': 'Size'})
value_S, value_L = self.env['product.attribute.value'].create([
{'name': 'S', 'attribute_id': attribute.id},
{'name': 'L', 'attribute_id': attribute.id}
])
product_tmpl = self.env['product.template'].create({
'name': 'Product Test Sync',
'type': 'consu',
'attribute_line_ids': [Command.create({
'attribute_id': attribute.id,
'value_ids': [Command.set([value_S.id, value_L.id])]
})]
})
[variant_s, variant_l] = product_tmpl.product_variant_ids
variant_s.default_code = 'zebra'
variant_l.default_code = 'alpaca'
bom = self.env['mrp.bom'].create({
'product_tmpl_id': product_tmpl.id,
'product_qty': 1.0,
'type': 'normal',
})
action_id = self.env.ref('mrp.action_report_mrp_bom')
url = "/web#action=%s&active_id=%s" % (str(action_id.id), str(bom.id))
self.start_tour(url, "mrp_bom_report_tour", login="admin")

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import common, Form
from odoo.exceptions import UserError
@ -24,14 +25,14 @@ class TestMrpMulticompany(common.TransactionCase):
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])],
'group_ids': [(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])],
'group_ids': [(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])]
})
@ -135,12 +136,12 @@ class TestMrpMulticompany(common.TransactionCase):
})
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))]}">
# The mo must be confirmed, no longer in draft, in order for `lot_producing_ids` to be visible in the view
# <div class="o_row" invisible="state == 'draft' or product_tracking in ('none', False)">
mo = mo_form.save()
mo.action_confirm()
mo_form = Form(mo)
mo_form.lot_producing_id = lot_b
mo_form.lot_producing_ids.set(lot_b)
mo = mo_form.save()
with self.assertRaises(UserError):
mo.with_user(self.user_b).action_confirm()
@ -176,8 +177,9 @@ class TestMrpMulticompany(common.TransactionCase):
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
ml.quantity = 1
details_operation_form.save()
mo.move_raw_ids.picked = True
with self.assertRaises(UserError):
mo.button_mark_done()
@ -205,8 +207,6 @@ class TestMrpMulticompany(common.TransactionCase):
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)
@ -255,7 +255,7 @@ class TestMrpMulticompany(common.TransactionCase):
""" 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)]})
self.user_a.write({'group_ids': [(4, group_stock_manager.id)]})
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture')
for rule in manufacture_route.rule_ids.sudo():
@ -282,3 +282,36 @@ class TestMrpMulticompany(common.TransactionCase):
'code': 'WH2',
})
self.assertEqual(new_warehouse.manufacture_pull_id.route_id.company_id, self.company_b)
def test_multi_company_kit_reservation(self):
"""
Create and assign a delivery in company_b for a product that is a kit in company_a.
Check that the move is treated just as a non-kit product.
"""
""" Check that is_kits is company dependant """
semi_kit_product = self.env['product.product'].create({
'name': 'Kit Kat',
'is_storable': True,
})
self.env['mrp.bom'].create([{
'product_id': semi_kit_product.id,
'product_tmpl_id': semi_kit_product.product_tmpl_id.id,
'company_id': self.company_a.id,
'type': 'phantom',
}])
warehouse_b = self.env['stock.warehouse'].search([('company_id', '=', self.company_b.id)], limit=1)
delivery = self.env['stock.picking'].with_company(self.company_b.id).create({
'picking_type_id': warehouse_b.out_type_id.id,
'location_id': warehouse_b.lot_stock_id.id,
'location_dest_id': self.ref('stock.stock_location_customers'),
'move_ids': [Command.create({
'product_id': semi_kit_product.id,
'product_uom_qty': 1,
'location_id': warehouse_b.lot_stock_id.id,
'location_dest_id': self.ref('stock.stock_location_customers'),
})]
})
# confirm and assign the delivery with company_a and check that it was treated as a non-kit product
delivery.with_company(self.company_a).action_confirm()
delivery.with_company(self.company_a).action_assign()
self.assertRecordValues(delivery.move_ids, [{'state': 'confirmed', 'quantity': 0.0}])

View file

@ -2,10 +2,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta, time
from freezegun import freeze_time
from pytz import timezone, utc
from odoo import fields
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tests import Form
class TestOee(TestMrpCommon):
@ -18,9 +20,26 @@ class TestOee(TestMrpCommon):
'description': loss_reason.name
})
def test_wrokcenter_oee(self):
@freeze_time('2025-05-30')
def test_unset_end_date(self):
with Form(self.env['mrp.workcenter.productivity']) as workcenter_productivity:
# Set the end date to tomorrow
workcenter_productivity.date_end = datetime(2025, 5, 31, 12, 0, 0)
# Unset the end date
workcenter_productivity.date_end = False
self.assertFalse(workcenter_productivity.date_end)
self.assertEqual(workcenter_productivity.duration, 0.0, "The duration should be 0.0 when the end date is unset.")
workcenter_productivity.workcenter_id = self.workcenter_1
workcenter_productivity.date_end = datetime(2025, 5, 31, 12, 0, 0)
workcenter_productivity.date_start = datetime(2025, 5, 30, 12, 0, 0)
workcenter_productivity.save()
self.assertEqual(workcenter_productivity.duration, 1440.0)
def test_workcenter_oee(self):
""" Test case workcenter oee. """
day = datetime.date(datetime.today())
self.workcenter_1.resource_calendar_id.leave_ids.unlink()
# Make the test work the weekend. It will fails due to workcenter working hours.
if day.weekday() in (5, 6):
day -= timedelta(days=2)
@ -57,15 +76,15 @@ class TestOee(TestMrpCommon):
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)
# Blocked time : ( Process Defect (1.33 min) + Reduced Speed (3.0 min) + Material Availability (1.52 min)) = 5.85 min
blocked_time = 1.33 + 3.0 + 1.52
# Productive time : Productive time duration (13 min)
productive_time_in_hour = round((13.0 / 60.0), 2)
productive_time = 13.0
# 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.")
# Blocked & Productive time are rounded to 2 digits when computed
self.assertEqual(self.workcenter_1.blocked_time, round(blocked_time / 60, 2), "Wrong blocked time on workcenter.")
self.assertEqual(self.workcenter_1.productive_time, round(productive_time / 60, 2), "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)
# OEE is not calculated with intermediary rounding
computed_oee = round((((productive_time / 60) * 100.0) / ((productive_time / 60) + (blocked_time / 60))), 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

@ -31,12 +31,12 @@ class TestMrpSerialMassProducePerformance(common.TransactionCase):
for i in range(raw_materials_count):
raw_materials.append(self.env['product.product'].create({
'name': '@raw_material#' + str(i + 1),
'type': 'product',
'is_storable': True,
'tracking': trackings[i % len(trackings)]
}))
finished = self.env['product.product'].create({
'name': '@finished',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
bom = self.env['mrp.bom'].create({
@ -70,7 +70,6 @@ class TestMrpSerialMassProducePerformance(common.TransactionCase):
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,
@ -83,7 +82,6 @@ class TestMrpSerialMassProducePerformance(common.TransactionCase):
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,
@ -94,7 +92,7 @@ class TestMrpSerialMassProducePerformance(common.TransactionCase):
mo.action_assign()
action = mo.action_serial_mass_produce_wizard()
action = mo.action_mass_produce()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
wizard.next_serial_number = "sn#1"
wizard.next_serial_count = quantity

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,55 @@
from odoo import Command
from odoo.exceptions import UserError
from odoo.addons.mrp.tests.common import TestMrpCommon
class TestMrpQuant(TestMrpCommon):
def test_kit_product_reservation_flow(self):
"""Test that kit products are not cleaned"""
product_kit = self.env['product.product'].create({
'name': 'This product will be a kit',
'type': 'consu',
'is_storable': True
})
delivery = self.env['stock.picking'].create({
'location_id': self.shelf_1.id,
'location_dest_id': self.warehouse_1.lot_stock_id.id,
'picking_type_id': self.picking_type_out.id,
'move_line_ids': [Command.create({
'product_id': product_kit.id,
'quantity_product_uom': 1,
'location_dest_id': self.warehouse_1.lot_stock_id.id,
})]
})
delivery.action_confirm()
self.env['mrp.bom'].create({
'product_tmpl_id': product_kit.product_tmpl_id.id,
'product_id': product_kit.id,
'product_qty': 1,
'type': 'phantom', # type kit
})
# Force recomputation of is_kits, since it's currently not recomputed automatically when BOM is created
product_kit._compute_is_kits()
delivery.move_ids.quantity = 1
product_normal = self.env['product.product'].create({
'name': 'Normal Product Test',
'type': 'consu',
'is_storable': True
})
# Should work without error
product_normal.action_open_quants()
# Ensure it's raise an error if we try to update quantity of kit product directly
with self.assertRaises(UserError, msg="You should update the components quantity instead of directly updating the quantity of the kit product."):
self.env['stock.quant']._update_available_quantity(
product_kit,
self.warehouse_1.lot_stock_id,
10,
)

View file

@ -0,0 +1,398 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from freezegun import freeze_time
from json import loads
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo import fields, Command
class TestMrpReplenish(TestMrpCommon):
def _create_wizard(self, product, warehouse):
return self.env['product.replenish'].with_context(default_product_tmpl_id=product.product_tmpl_id.id).create({
'product_id': product.id,
'product_uom_id': self.uom_unit.id,
'quantity': 1,
'warehouse_id': warehouse.id,
})
def test_mrp_delay(self):
"""Open the replenish view and check if delay is taken into account
in the base date computation
"""
route = self.warehouse_1.manufacture_pull_id.route_id
product = self.product_4
product.route_ids = route
with freeze_time("2023-01-01"):
wizard = self._create_wizard(product, self.warehouse_1)
self.assertEqual(fields.Datetime.from_string('2023-01-01 00:00:00'), wizard.date_planned)
route.rule_ids[0].delay = 2
wizard3 = self._create_wizard(product, self.warehouse_1)
self.assertEqual(fields.Datetime.from_string('2023-01-03 00:00:00'), wizard3.date_planned)
def test_mrp_orderpoint_leadtime(self):
self.env.company.horizon_days = 0
route_manufacture = self.warehouse_1.manufacture_pull_id.route_id
route_manufacture.supplied_wh_id = self.warehouse_1
route_manufacture.supplier_wh_id = self.warehouse_1
route_manufacture.rule_ids.delay = 2
product_1 = self.env['product.product'].create({
'name': 'Cake',
'is_storable': True,
'route_ids': [(6, 0, [route_manufacture.id])]
})
self.env['mrp.bom'].create({
'product_tmpl_id': product_1.product_tmpl_id.id,
'produce_delay': 4,
'product_qty': 1,
})
# setup orderpoint (reordering rule)
rr = self.env['stock.warehouse.orderpoint'].create({
'name': 'Cake RR',
'location_id': self.stock_location.id,
'product_id': product_1.id,
'product_min_qty': 0,
'product_max_qty': 5,
})
info = self.env['stock.replenishment.info'].create({'orderpoint_id': rr.id})
# for manufacturing delay should be taken from the bom
self.assertEqual("4.0 days", info.wh_replenishment_option_ids.lead_time)
def test_rr_picking_type_id(self):
"""Check manufacturing order take bom according to picking type of the rule triggered by an
orderpoint."""
self.product_4.route_ids = self.warehouse_1.manufacture_pull_id.route_id
picking_type_2 = self.picking_type_manu.copy({'sequence': 100})
self.product_4.bom_ids.picking_type_id = picking_type_2
rr = self.env['stock.warehouse.orderpoint'].create({
'name': 'Cake RR',
'location_id': self.warehouse_1.lot_stock_id.id,
'product_id': self.product_4.id,
'product_min_qty': 5,
'product_max_qty': 10,
})
rr.action_replenish()
mo = self.env['mrp.production'].search([
('product_id', '=', self.product_4.id),
('picking_type_id', '=', picking_type_2.id)
])
self.assertTrue(mo)
def test_mrp_delay_bom(self):
route = self.warehouse_1.manufacture_pull_id.route_id
product = self.product_4
bom = product.bom_ids
product.route_ids = route
with freeze_time("2023-01-01"):
wizard = self._create_wizard(product, self.warehouse_1)
self.assertEqual(fields.Datetime.from_string('2023-01-01 00:00:00'), wizard.date_planned)
bom.produce_delay = 2
wizard2 = self._create_wizard(product, self.warehouse_1)
self.assertEqual(fields.Datetime.from_string('2023-01-03 00:00:00'), wizard2.date_planned)
bom.days_to_prepare_mo = 4
wizard3 = self._create_wizard(product, self.warehouse_1)
self.assertEqual(fields.Datetime.from_string('2023-01-07 00:00:00'), wizard3.date_planned)
def test_replenish_from_scrap(self):
""" Test that when ticking replenish on the scrap wizard of a MO, the new move
is linked to the MO and validating it will automatically reserve the quantity
on the MO. """
self.warehouse_1.manufacture_steps = 'pbm'
basic_mo, dummy1, dummy2, product_to_scrap, other_product = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
for product in (product_to_scrap, other_product):
self.env['stock.quant'].create({
'product_id': product.id,
'location_id': self.stock_location.id,
'quantity': 2
})
self.assertEqual(basic_mo.move_raw_ids.location_id, self.warehouse_1.pbm_loc_id)
basic_mo.action_confirm()
self.assertEqual(len(basic_mo.picking_ids), 1)
basic_mo.picking_ids.action_assign()
basic_mo.picking_ids.button_validate()
self.assertEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
scrap_form = Form.from_action(self.env, basic_mo.button_scrap())
scrap_form.product_id = product_to_scrap
scrap_form.should_replenish = True
self.assertEqual(scrap_form.location_id, self.warehouse_1.pbm_loc_id)
scrap_form.save().action_validate()
self.assertNotEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
self.assertEqual(len(basic_mo.picking_ids), 2)
replenish_picking = basic_mo.picking_ids.filtered(lambda x: x.state == 'assigned')
replenish_picking.button_validate()
self.assertEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
def test_scrap_replenishment_reassigns_required_qty_to_component(self):
""" Test that when validating the scrap replenishment transfer, the required quantity
is re-assigned to the component for the final manufacturing product. """
self.warehouse_1.manufacture_steps = 'pbm'
basic_mo, _, _, product_to_scrap, other_product = self.generate_mo(qty_final=1, qty_base_1=10, qty_base_2=10)
for product in (product_to_scrap, other_product):
self.env['stock.quant'].create({
'product_id': product.id,
'location_id': self.stock_location.id,
'quantity': 20
})
self.assertEqual(basic_mo.move_raw_ids.location_id, self.warehouse_1.pbm_loc_id)
basic_mo.action_confirm()
self.assertEqual(len(basic_mo.picking_ids), 1)
basic_mo.picking_ids.action_assign()
basic_mo.picking_ids.button_validate()
self.assertEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
# Scrap the product and trigger replenishment
scrap_form = Form.from_action(self.env, basic_mo.button_scrap())
scrap_form.product_id = product_to_scrap
scrap_form.scrap_qty = 5
scrap_form.should_replenish = True
self.assertEqual(scrap_form.location_id, self.warehouse_1.pbm_loc_id)
scrap_form.save().action_validate()
# Assert that the component quantity is reduced
self.assertNotEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
move_to_scrap = basic_mo.move_raw_ids.filtered(lambda m: m.product_id == product_to_scrap)
move_other = basic_mo.move_raw_ids.filtered(lambda m: m.product_id == other_product)
self.assertEqual(move_to_scrap.quantity, 5, "Scrapped component should have qty 5")
self.assertEqual(move_other.quantity, 10, "Other component should remain qty 10")
self.assertEqual(len(basic_mo.picking_ids), 2)
replenish_picking = basic_mo.picking_ids.filtered(lambda x: x.state == 'assigned')
replenish_picking.button_validate()
# Assert that the component quantity is re-assigned
self.assertEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
self.assertEqual(move_to_scrap.quantity, 10, "Scrapped component should return to qty 10")
self.assertEqual(move_other.quantity, 10, "Other component should still be qty 10")
def test_global_horizon_days_affect_lead_time_manufacture_rule(self):
""" Ensure global horizon days will only be captured one time in an orderpoint's
lead_days/json_lead_days.
"""
self.warehouse_1.manufacture_steps = 'pbm'
finished_product = self.product_4
finished_product.route_ids = [Command.set(self.warehouse_1.manufacture_pull_id.route_id.ids)]
orderpoint = self.env['stock.warehouse.orderpoint'].create({
'product_id': finished_product.id,
'location_id': self.stock_location.id,
})
out_picking = self.env['stock.picking'].create({
'picking_type_id': self.picking_type_out.id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'move_ids': [Command.create({
'product_id': finished_product.id,
'product_uom_qty': 2,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
})],
})
out_picking.with_context(global_horizon_days=365).action_assign()
r = orderpoint.action_stock_replenishment_info()
repl_info = self.env[r['res_model']].browse(r['res_id'])
lead_horizon_date = datetime.strptime(
loads(repl_info.json_lead_days)['lead_horizon_date'], '%m/%d/%Y').date()
self.assertEqual(lead_horizon_date, fields.Date.today() + timedelta(days=365))
def test_orderpoint_onchange_reordering_rule(self):
""" Ensure onchange logic works properly when editing a reordering rule
linked to a confirmed MO, which is started but not finished by the
end of the stock forecast.
"""
route_manufacture = self.warehouse_1.manufacture_pull_id.route_id
self.product_4.route_ids = [Command.set([route_manufacture.id])]
self.product_4.bom_ids.produce_delay = 2
orderpoint = self.env['stock.warehouse.orderpoint'].create({
'product_id': self.product_4.id,
'product_min_qty': 2,
'product_max_qty': 2,
})
orderpoint.action_replenish()
prod = self.env['mrp.production'].search([('origin', '=', orderpoint.name)])
# Error is triggered for date_start <= lead_horizon_date < date_finished
prod.date_start = fields.Date.today() + timedelta(days=1)
with Form(orderpoint, view='stock.view_warehouse_orderpoint_tree_editable') as form:
form.product_min_qty = 3
self.assertEqual(orderpoint.qty_to_order, 1)
orderpoint.trigger = 'manual'
with Form(orderpoint, view='stock.view_warehouse_orderpoint_tree_editable') as form:
form.product_min_qty = 10
self.assertEqual(form.qty_to_order, 0)
self.assertEqual(form.qty_to_order, 8)
self.assertEqual(orderpoint.qty_to_order, 8)
def test_replenish_multi_level_bom_with_pbm_sam(self):
"""
Ensure that in a 3-step manufacturing flow ('pbm_sam') with MTO + reordering rule,
a multi-level BOM triggers separate MOs for each level without constraint errors.
1.) Set warehouse manufacture to (manufacture_steps == 'pbm_sam')
2.) Product_1 (enable manufacture and mto routes)
3.) Product_4 (enable manufacture)
4.) Add Product_1 as bom for product_4
5.) Add a reordering rule (manufacture) for product_4
6.) trigger replenishment for product_4
"""
self.warehouse = self.env.ref('stock.warehouse0')
self.warehouse.write({'manufacture_steps': 'pbm_sam'})
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_1.write({
'route_ids': [(6, 0, [route_mto, route_manufacture])]
}) # Component
self.product_4.write({
'route_ids': [(6, 0, [route_manufacture])],
'bom_ids': [(6, 0, [self.bom_1.id])]
}) # Finished Product
# Create reordering rule
self.env['stock.warehouse.orderpoint'].create({
'location_id': self.warehouse.lot_stock_id.id,
'product_id': self.product_4.id,
'route_id': route_manufacture,
'product_min_qty': 1,
'product_max_qty': 1,
})
self.product_4.orderpoint_ids.action_replenish()
# Check both MOs were created
mo_final = self.env['mrp.production'].search([('product_id', '=', self.product_4.id)])
mo_component = self.env['mrp.production'].search([('product_id', '=', self.product_1.id)])
self.assertEqual(len(mo_final), 1, "Expected one MO for the final product.")
self.assertEqual(len(mo_component), 1, "Expected one MO for the manufactured BOM component.")
def test_orderpoint_warning_mrp(self):
""" Checks that the warning correctly computes depending on if there's a bom. """
orderpoint = self.env['stock.warehouse.orderpoint'].create({
'product_id': self.product_4.id,
'product_min_qty': 10,
'product_max_qty': 50,
})
self.product_4.bom_ids.active = False
self.assertTrue(orderpoint.show_supply_warning)
# Archive the boms linked to the product
self.product_4.with_context(active_test=False).bom_ids.active = True
orderpoint.invalidate_recordset(fnames=['rule_ids', 'show_supply_warning'])
self.assertFalse(orderpoint.show_supply_warning)
# Add a manufacture route to the product
self.product_4.route_ids |= self.route_manufacture
orderpoint.invalidate_recordset(fnames=['show_supply_warning'])
self.assertFalse(orderpoint.show_supply_warning)
def test_set_bom_on_orderpoint(self):
""" Test that action_set_bom_on_orderpoint correctly sets a bom on selected orderpoint. """
orderpoint = self.env['stock.warehouse.orderpoint'].create({
'product_id': self.product_4.id,
'product_min_qty': 10,
'product_max_qty': 50,
})
self.product_4.bom_ids.with_context(orderpoint_id=orderpoint.id).action_set_bom_on_orderpoint()
self.assertEqual(orderpoint.bom_id.id, self.product_4.bom_ids.id)
def test_effective_bom(self):
route = self.env['stock.route'].create({
'name': 'Manufacture',
'rule_ids': [
Command.create({
'name': 'Manufacture',
'location_dest_id': self.stock_location.id,
'action': 'manufacture',
'picking_type_id': self.picking_type_in.id,
}),
],
})
orderpoint = self.env['stock.warehouse.orderpoint'].create({
'product_id': self.productA.id,
'product_min_qty': 2,
'product_max_qty': 4,
})
# The Manufacture route is not set on the product -> no effective BoM
self.assertFalse(orderpoint.effective_bom_id)
self.productA.write({
'route_ids': [(4, route.id)],
})
self.env.invalidate_all()
# The route is set, but there is no BoM -> no effective BoM
self.assertFalse(orderpoint.effective_bom_id)
bom = self.env['mrp.bom'].create({
'product_tmpl_id': self.productA.product_tmpl_id.id,
'product_qty': 1,
'code': 'Ref 1234',
})
self.env.invalidate_all()
# The route is set and there is a BoM -> effective BoM is available
self.assertEqual(orderpoint.effective_bom_id, bom)
self.assertEqual(orderpoint.bom_id_placeholder, 'Ref 1234: Product A')
# The actual BoM remains empty
self.assertFalse(orderpoint.bom_id)
def test_lead_time_with_no_bom(self):
"""Test that lead time is incremented by 365 days (1 year) when there
is no BoM defined.
"""
route_manufacture = self.warehouse_1.manufacture_pull_id.route_id
product = self.env['product.product'].create({
'name': 'test',
'is_storable': True,
'route_ids': route_manufacture.ids,
})
orderpoint = self.env['stock.warehouse.orderpoint'].create({
'name': 'test',
'location_id': self.warehouse_1.lot_stock_id.id,
'product_id': product.id,
'product_min_qty': 0,
'product_max_qty': 5,
})
self.assertEqual(orderpoint.lead_days, 365)
def test_orderpoint_with_kit_bom_in_another_company(self):
"""Test that an orderpoint can be created for a product
having a kit-type BoM defined in another company.
"""
self.assertEqual(self.bom_2.type, 'phantom')
self.assertEqual(self.bom_2.company_id, self.env.company)
company_2 = self.env['res.company'].create({'name': 'Company 2'})
orderpoint = self.env['stock.warehouse.orderpoint'].with_company(company_2).create({
'product_id': self.bom_2.product_id.id,
})
self.assertEqual(orderpoint.company_id, company_2)
def test_product_replenish_wizard_multiple_manufacture_routes(self):
self.route_manufacture.copy()
wizard_form = Form(self.env['product.replenish'].with_context(
default_product_tmpl_id=self.product_4.product_tmpl_id.id
))
manufacture_route = self.env['stock.rule'].search([
('action', '=', 'manufacture'),
('company_id', '=', self.company.id),
('location_dest_id.usage', '=', 'internal'),
], limit=1).route_id
self.assertEqual(wizard_form.route_id, manufacture_route)

View file

@ -1,269 +0,0 @@
# -*- 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

@ -12,8 +12,20 @@ class TestWarehouseMrp(common.TestMrpCommon):
def setUpClass(cls):
super().setUpClass()
unit = cls.env.ref("uom.product_uom_unit")
cls.stock_location = cls.env.ref('stock.stock_location_stock')
cls.graphics_card = cls.env['product.product'].create({
'name': 'Individual Workplace',
'uom_id': cls.uom_unit.id,
'type': 'consu',
'is_storable': True,
'tracking': 'none',
})
cls.laptop = cls.env['product.product'].create({
'name': 'Acoustic Bloc Screens',
'uom_id': cls.uom_unit.id,
'type': 'consu',
'is_storable': True,
'tracking': 'none',
})
cls.depot_location = cls.env['stock.location'].create({
'name': 'Depot',
'usage': 'internal',
@ -22,14 +34,13 @@ class TestWarehouseMrp(common.TestMrpCommon):
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,
'location_id': cls.shelf_1.id,
'product_id': cls.graphics_card.id,
'inventory_quantity': 16.0
}).action_apply_inventory()
@ -37,16 +48,19 @@ class TestWarehouseMrp(common.TestMrpCommon):
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,
'product_uom_id': cls.uom_unit.id,
'consumption': 'flexible',
'bom_line_ids': [(0, 0, {
'bom_line_ids': [Command.create({
'product_id': cls.graphics_card.id,
'product_qty': 1,
'product_uom_id': unit.id
'product_uom_id': cls.uom_unit.id
})],
'operation_ids': [Command.create({
'name': 'Cutting Machine',
'workcenter_id': cls.workcenter_1.id,
'time_cycle': 12,
'sequence': 1,
})],
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
],
})
def new_mo_laptop(self):
@ -67,22 +81,114 @@ class TestWarehouseMrp(common.TestMrpCommon):
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
})
warehouse_1_stock_manager.manufacture_to_resupply = False
self.assertFalse(self.warehouse_1.manufacture_pull_id.active)
self.assertFalse(self.warehouse_1.manu_type_id.active)
self.assertFalse(self.picking_type_manu.active)
self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes())
warehouse_1_stock_manager.write({
'manufacture_to_resupply': True
})
warehouse_1_stock_manager.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.assertTrue(self.picking_type_manu.active)
self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
def test_manufacturing_rule_other_dest(self):
""" Ensures that a manufacturing rule can define a destination the rule itself and have it
applied instead of the one from the operation type if location_dest_from_rule is set.
"""
freezer_loc = self.env['stock.location'].create({
'name': 'Freezer',
'location_id': self.warehouse_1.view_location_id.id,
})
route = self.env['stock.route'].create({
'name': 'Manufacture then freeze',
'rule_ids': [
Command.create({
'name': 'Freezer -> Stock',
'action': 'pull',
'procure_method': 'make_to_order',
'picking_type_id': self.warehouse_1.int_type_id.id,
'location_src_id': freezer_loc.id,
'location_dest_id': self.warehouse_1.lot_stock_id.id,
'location_dest_from_rule': True,
}),
Command.create({
'name': 'Manufacture',
'action': 'manufacture',
'picking_type_id': self.warehouse_1.manu_type_id.id,
'location_src_id': self.warehouse_1.lot_stock_id.id,
'location_dest_id': freezer_loc.id,
'location_dest_from_rule': True,
}),
],
})
# Remove the classic Manufacture route if it exists and replace it by the new one
self.product_4.route_ids = [
Command.link(route.id),
Command.unlink(self.warehouse_1.manufacture_pull_id.id),
]
# Create a procurement to resupply the Stock, taking from the Freezer.
self.env['stock.rule'].run([
self.env['stock.rule'].Procurement(
self.product_4,
5.0,
self.product_4.uom_id,
self.warehouse_1.lot_stock_id,
'test_other_dest',
'test_other_dest',
self.warehouse_1.company_id,
{
'warehouse_id': self.warehouse_1,
}
)
])
# Make sure the production is delivering the goods in the location set on the rule.
production = self.env['mrp.production'].search([('product_id', '=', self.product_4.id)])
self.assertEqual(len(production), 1)
self.assertEqual(production.picking_type_id.default_location_dest_id, self.warehouse_1.lot_stock_id)
self.assertEqual(production.location_dest_id, freezer_loc)
def test_multi_warehouse_resupply(self):
""" test a multi warehouse flow give a correct date delay
product_6 is sold from warehouse_1, its component (product_4) is
resupplied from warehouse_2 and manufactured in warehouse_2.
Everything in mto """
self.route_mto.active = True
warehouse_2 = self.env['stock.warehouse'].create({
'name': 'Warehouse 2',
'code': 'WH2',
})
# product 4 can only be manufacture in WH2
self.bom_1.picking_type_id = warehouse_2.manu_type_id
self.warehouse_1.manufacture_steps = "pbm"
self.warehouse_1.resupply_wh_ids = [Command.set([warehouse_2.id])]
self.product_6.route_ids = [Command.set([self.route_manufacture.id, self.route_mto.id])]
self.product_4.route_ids = [Command.set([
self.warehouse_1.resupply_route_ids.id,
self.route_mto.id,
])]
warehouse_2.resupply_route_ids.rule_ids.procure_method = 'make_to_order'
self.env['stock.rule'].run([
self.env['stock.rule'].Procurement(
self.product_6,
5.0,
self.product_6.uom_id,
self.customer_location,
'test_ressuply',
'test_ressuply',
self.warehouse_1.company_id,
{
'warehouse_id': self.warehouse_1,
},
),
])
def test_manufacturing_scrap(self):
"""
Testing to do a scrap of consumed material.
@ -100,17 +206,15 @@ class TestWarehouseMrp(common.TestMrpCommon):
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,
'location_id': self.shelf_1.id,
'product_id': self.product_4.id,
'inventory_quantity': 8,
'lot_id': lot_product_4.id
@ -118,7 +222,7 @@ class TestWarehouseMrp(common.TestMrpCommon):
# Inventory for Stone Tools
self.env['stock.quant'].create({
'location_id': self.stock_location_14.id,
'location_id': self.shelf_1.id,
'product_id': self.product_2.id,
'inventory_quantity': 12,
'lot_id': lot_product_2.id
@ -147,17 +251,17 @@ class TestWarehouseMrp(common.TestMrpCommon):
# 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
scrap_move = scrap_id.move_ids[0]
self.assertTrue(scrap_move.raw_material_production_id)
self.assertTrue(scrap_move.scrapped)
self.assertEqual(scrap_move.location_dest_usage, 'inventory')
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)
# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.location_dest_usage == 'inventory')
# self.assertTrue(scrap_move, "There are no any scrap move created for production order.")
def test_putaway_after_manufacturing_3(self):
@ -167,11 +271,10 @@ class TestWarehouseMrp(common.TestMrpCommon):
"""
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})
serial = self.env['stock.lot'].create({'product_id': self.laptop.id})
mo_form = Form(mo_laptop)
mo_form.qty_producing = 1
mo_form.lot_producing_id = serial
mo_form.lot_producing_ids.set(serial)
mo_laptop = mo_form.save()
mo_laptop.button_mark_done()
@ -183,10 +286,9 @@ class TestWarehouseMrp(common.TestMrpCommon):
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.warehouse_1.manufacture_steps = 'pbm'
self.product_1.type = 'product'
self.product_1.is_storable = True
self.env['stock.quant']._update_available_quantity(self.product_1, self.stock_location, 100)
mo_form = Form(self.env['mrp.production'])
@ -195,25 +297,30 @@ class TestWarehouseMrp(common.TestMrpCommon):
mo = mo_form.save()
mo.action_confirm()
package = self.env['stock.quant.package'].create({})
package = self.env['stock.package'].create({})
picking = mo.picking_ids
picking.move_line_ids.write({
'qty_done': 20,
'quantity': 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()
Form.from_action(self.env, picking.button_validate()).save().process()
backorder = picking.backorder_ids
backorder.move_line_ids.qty_done = 80
backorder.move_line_ids.quantity = 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])
self.assertEqual(mo.move_raw_ids.move_line_ids.mapped('quantity_product_uom'), [20, 80])
def test_produce_with_zero_available_qty(self):
""" Test that producing with 0 qty_available for the component
still links the stock.move.line to the production order. """
mo, *_ = self.generate_mo()
mo.button_mark_done()
self.assertEqual(mo.move_raw_ids.move_line_ids.production_id, mo)
def test_unarchive_mto_route_active_needed_rules_only(self):
""" Ensure that activating a route will activate only its relevant rules.
@ -221,33 +328,32 @@ class TestWarehouseMrp(common.TestMrpCommon):
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')
self.env.user.group_ids += self.env.ref('stock.group_adv_location')
# 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)
self.assertFalse(self.route_mto.active)
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, self.route_mto.rule_ids)
# Activate the MTO route and still 'WH: Stock → Pre-Production (MTO)' is not shown in MTO route.
mto_route.active = True
self.route_mto.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)
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, self.route_mto.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)
self.assertIn(self.warehouse_1.pbm_mto_pull_id, self.route_mto.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)
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, self.route_mto.rule_ids)
class TestKitPicking(common.TestMrpCommon):
@ -258,7 +364,7 @@ class TestKitPicking(common.TestMrpCommon):
def create_product(name):
p = Form(cls.env['product.product'])
p.name = name
p.detailed_type = 'product'
p.is_storable = True
return p.save()
# Create a kit 'kit_parent' :
@ -282,13 +388,13 @@ class TestKitPicking(common.TestMrpCommon):
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_1 = create_product('Kit 1')
cls.kit_2 = create_product('Kit 2')
cls.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_tmpl_id': cls.kit_1.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine = cls.env['mrp.bom.line']
@ -305,7 +411,7 @@ class TestKitPicking(common.TestMrpCommon):
'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_tmpl_id': cls.kit_2.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine.create({
@ -313,7 +419,7 @@ class TestKitPicking(common.TestMrpCommon):
'product_qty': 1.0,
'bom_id': bom_kit_2.id})
BomLine.create({
'product_id': kit_1.id,
'product_id': cls.kit_1.id,
'product_qty': 2.0,
'bom_id': bom_kit_2.id})
bom_kit_parent = cls.env['mrp.bom'].create({
@ -325,11 +431,11 @@ class TestKitPicking(common.TestMrpCommon):
'product_qty': 1.0,
'bom_id': bom_kit_parent.id})
BomLine.create({
'product_id': kit_2.id,
'product_id': cls.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_tmpl_id': cls.kit_3.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine.create({
@ -341,7 +447,7 @@ class TestKitPicking(common.TestMrpCommon):
'product_qty': 2.0,
'bom_id': bom_kit_3.id})
BomLine.create({
'product_id': kit_3.id,
'product_id': cls.kit_3.id,
'product_qty': 1.0,
'bom_id': bom_kit_parent.id})
@ -352,7 +458,7 @@ class TestKitPicking(common.TestMrpCommon):
cls.test_supplier = cls.env['stock.location'].create({
'name': 'supplier',
'usage': 'supplier',
'location_id': cls.env.ref('stock.stock_location_stock').id,
'location_id': cls.stock_location.id,
})
cls.expected_quantities = {
@ -366,23 +472,22 @@ class TestKitPicking(common.TestMrpCommon):
}
def test_kit_immediate_transfer(self):
""" Make sure a kit is split in the corrects quantity_done by components in case of an
""" Make sure a kit is split in the corrects quantity 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
'picking_type_id': self.picking_type_in.id,
})
move_receipt_1 = self.env['stock.move'].create({
'name': self.kit_parent.name,
self.env['stock.move'].create({
'product_id': self.kit_parent.id,
'quantity_done': 3,
'quantity': 3,
'picked': True,
'product_uom': self.kit_parent.uom_id.id,
'picking_id': picking.id,
'picking_type_id': self.env.ref('stock.picking_type_in').id,
'picking_type_id': self.picking_type_in.id,
'location_id': self.test_supplier.id,
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
})
@ -390,8 +495,9 @@ class TestKitPicking(common.TestMrpCommon):
# 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])
for move in picking.move_ids:
self.assertEqual(move.quantity, self.expected_quantities[move.product_id])
self.assertEqual(move.state, 'done')
def test_kit_planned_transfer(self):
""" Make sure a kit is split in the corrects product_qty by components in case of a
@ -401,16 +507,14 @@ class TestKitPicking(common.TestMrpCommon):
'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,
'picking_type_id': self.picking_type_in.id,
})
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,
'picking_type_id': self.picking_type_in.id,
'location_id': self.test_supplier.id,
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
})
@ -423,56 +527,53 @@ class TestKitPicking(common.TestMrpCommon):
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'})
product = self.env['product.product'].create({'name': 'Super Product', 'is_storable': True})
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,
'location_id': self.customer_location.id,
'location_dest_id': self.stock_location.id,
'move_ids': [Command.create({
'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,
'location_id': self.customer_location.id,
'location_dest_id': self.stock_location.id,
})]
})
receipt.action_confirm()
receipt.move_line_ids.qty_done = 1
receipt.move_line_ids = [(0, 0, {
receipt.move_line_ids.quantity = 1
receipt.move_line_ids = [Command.create({
'product_id': kit.id,
'qty_done': 1,
'quantity': 1,
'product_uom_id': kit.uom_id.id,
'location_id': customer_location.id,
'location_dest_id': stock_location.id,
'location_id': self.customer_location.id,
'location_dest_id': self.stock_location.id,
})]
receipt.move_ids.picked = True
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'},
{'product_id': product.id, 'quantity': 1, 'state': 'done'},
{'product_id': compo.id, 'quantity': 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,
'is_storable': True,
'uom_id': self.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({
@ -494,25 +595,25 @@ class TestKitPicking(common.TestMrpCommon):
})
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:
delivery_form.picking_type_id = self.picking_type_in
with delivery_form.move_ids.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:
with delivery_form.move_ids.new() as move:
move.product_id = not_kit_1
move.product_uom_qty = 4
with delivery_form.move_ids_without_package.new() as move:
with delivery_form.move_ids.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
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_1).quantity = 3
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_2).quantity = 3
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_1).quantity = 4
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_2).quantity = 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 = Form.from_action(self.env, backorder_wizard_dict)
backorder_wizard_form.save().process_cancel_backorder()
aggregate_not_kit_values = delivery.move_line_ids._get_aggregated_product_quantities()
@ -522,3 +623,139 @@ class TestKitPicking(common.TestMrpCommon):
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')
def test_scrap_consu_kit_not_available(self):
"""
Scrap a consumable kit with one product not available in stock
"""
self._test_scrap_kit_not_available(False)
def test_scrap_storable_kit_not_available(self):
"""
Scrap a storable kit with one product not available in stock
"""
self._test_scrap_kit_not_available(True)
def _test_scrap_kit_not_available(self, storable):
bom = self.bom_4
bom.type = 'phantom'
kit = bom.product_id
component = bom.bom_line_ids.product_id
kit.is_storable = storable
component.is_storable = True
scrap = self.env['stock.scrap'].create({
'product_id': kit.id,
'product_uom_id': kit.uom_id.id,
'scrap_qty': 1,
'bom_id': bom.id,
})
Form.from_action(self.env, scrap.action_validate()).save().action_done()
self.assertEqual(scrap.state, 'done')
self.assertRecordValues(scrap.move_ids, [
{'product_id': component.id, 'quantity': 1, 'state': 'done'}
])
def test_kit_with_packaging_different_uom(self):
"""
Test that a quantity packaging is correctly computed on a move line
when a kit is in a different uom than its components.
- Component(uom=Kg)
- Kit (uom=unit) -> Bom (1 dozen) -> Component (10 g)
- Packaging (qty=2 units of kit)
"""
bom = self.bom_4
bom.product_id = False
bom.type = 'phantom'
kit = bom.product_tmpl_id.product_variant_id
kit.is_storable = True
# product is in unit and bom in dozen
kit.uom_id = self.uom_unit
bom.product_uom_id = self.uom_dozen
bom.product_qty = 1
# create a packaging with 2 units
packaging = self.env['uom.uom'].create({
'name': 'Pack of 2',
'relative_factor': 2,
'relative_uom_id': self.env.ref('uom.product_uom_unit').id,
})
# component is in Kg but bom_line in gram
component = bom.bom_line_ids.product_id
component.uom_id = self.uom_kg
bom.bom_line_ids.product_uom_id = self.uom_gram
bom.bom_line_ids.product_qty = 10
# create a delivery with 20 units of kit
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
stock_location = warehouse.lot_stock_id
delivery = self.env['stock.picking'].create({
'picking_type_id': self.picking_type_out.id,
'location_id': stock_location.id,
'location_dest_id': self.customer_location.id,
'move_ids': [Command.create({
'product_id': kit.id,
'product_uom_qty': 12,
'product_uom': packaging.id,
'location_id': stock_location.id,
'location_dest_id': self.customer_location.id,
})],
})
delivery.action_confirm()
self.assertEqual(delivery.move_ids.product_id, component)
self.assertEqual(delivery.move_ids.product_uom_qty, 20)
delivery.move_ids.quantity = 20
delivery.move_ids.picked = True
delivery.button_validate()
self.assertTrue(delivery.state, 'done')
def test_search_kit_on_quantity(self):
self.env['stock.quant'].create([{
'product_id': product.id,
'inventory_quantity': qty,
'location_id': self.test_supplier.id,
} for product, qty in self.expected_quantities.items()]).action_apply_inventory()
products = self.env['product.product'].search([
'&', ('qty_available', '>', 3), ('qty_available', '<', 9),
])
self.assertNotIn(self.kit_1, products) # 12
self.assertIn(self.kit_2, products) # 6
self.assertNotIn(self.kit_3, products) # 3
def test_scrap_change_product(self):
""" Ensure a scrap order automatically updates the BoM when its product is changed,
selecting the product's first BoM if it's a kit or set the field empty otherwise."""
bom_a = self.bom_1
bom_a.type = 'phantom'
product_a = bom_a.product_id
bom_b = self.bom_3
bom_b.type = 'phantom'
product_b = bom_b.product_id
product_c = self.env['product.product'].create({'name': 'product_c', 'is_storable': True})
form = Form(self.env['stock.scrap'])
form.product_id = product_a
form.bom_id = bom_a
form.scrap_qty = 1
scrap = form.save()
# assert the scrap's bom_id is set to bom_a
self.assertEqual(scrap.bom_id, bom_a)
form.product_id = product_b
scrap = form.save()
# assert the scrap's bom_id is set to bom_b after updating the product
self.assertEqual(scrap.bom_id, bom_b)
form.product_id = product_c
scrap = form.save()
# assert the scrap's bom_id is updated to False after updating the product
self.assertFalse(scrap.bom_id)

View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import Form
from odoo.tests import Form
from odoo.addons.stock.tests.test_report import TestReportsCommon
from odoo import Command
class TestMrpStockReports(TestReportsCommon):
@ -14,14 +15,19 @@ class TestMrpStockReports(TestReportsCommon):
product_chocolate = self.env['product.product'].create({
'name': 'Chocolate',
'type': 'consu',
'standard_price': 10
})
product_chococake = self.env['product.product'].create({
'name': 'Choco Cake',
'type': 'product',
'is_storable': True,
})
product_double_chococake = self.env['product.product'].create({
'name': 'Double Choco Cake',
'type': 'product',
'is_storable': True,
})
byproduct = self.env['product.product'].create({
'name': 'by-product',
'is_storable': True,
})
# Creates two BOM: one creating a regular slime, one using regular slimes.
@ -34,6 +40,8 @@ class TestMrpStockReports(TestReportsCommon):
'bom_line_ids': [
(0, 0, {'product_id': product_chocolate.id, 'product_qty': 4}),
],
'byproduct_ids':
[(0, 0, {'product_id': byproduct.id, 'product_qty': 2, 'cost_share': 1.8})],
})
bom_double_chococake = self.env['mrp.bom'].create({
'product_id': product_double_chococake.id,
@ -59,9 +67,9 @@ class TestMrpStockReports(TestReportsCommon):
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.")
draft_picking_qty = self.sum_dicts(docs['product'], 'draft_picking_qty')
draft_production_qty = self.sum_dicts(docs['product'], 'draft_production_qty')
self.assertEqual(len(lines), 1, "Must have 1 line.")
self.assertEqual(draft_picking_qty['in'], 0)
self.assertEqual(draft_picking_qty['out'], 0)
self.assertEqual(draft_production_qty['in'], 10)
@ -71,15 +79,15 @@ class TestMrpStockReports(TestReportsCommon):
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']
draft_picking_qty = self.sum_dicts(docs['product'], 'draft_picking_qty')
draft_production_qty = self.sum_dicts(docs['product'], '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['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_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)
@ -87,6 +95,25 @@ class TestMrpStockReports(TestReportsCommon):
self.assertEqual(draft_production_qty['in'], 0)
self.assertEqual(draft_production_qty['out'], 0)
mo_form = Form(mo_1)
mo_form.qty_producing = 10
mo_form.save()
mo_1.move_byproduct_ids.quantity = 18
mo_1.button_mark_done()
self.env.flush_all() # flush to correctly build report
report_values = self.env['report.mrp.report_mo_overview']._get_report_data(mo_1.id)
self.assertEqual(report_values['byproducts']['details'][0]['name'], byproduct.name)
self.assertEqual(report_values['byproducts']['details'][0]['quantity'], 18)
# (Component price $10) * (4 unit to produce one finished) * (the mo qty = 10 units) = $400
self.assertEqual(report_values['components'][0]['summary']['mo_cost'], 400)
# cost_share of byproduct = 1.8 -> 1.8 / 100 -> 0.018 * 400 = 7.2
self.assertAlmostEqual(report_values['byproducts']['summary']['mo_cost'], 7.2)
byproduct_report_values = report_values['cost_breakdown'][1]
self.assertEqual(byproduct_report_values['name'], byproduct.name)
# 7.2 / 18 units = 0.4
self.assertAlmostEqual(byproduct_report_values['unit_avg_total_cost'], 0.4)
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.
@ -97,7 +124,7 @@ class TestMrpStockReports(TestReportsCommon):
# Configures a product.
product_apple_pie = self.env['product.product'].create({
'name': 'Apple Pie',
'type': 'product',
'is_storable': True,
})
product_apple = self.env['product.product'].create({
'name': 'Apple',
@ -123,8 +150,9 @@ class TestMrpStockReports(TestReportsCommon):
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
with Form(pick.move_ids, view='stock.view_stock_move_operations') as form:
with form.move_line_ids.edit(0) as move_line:
move_line.quantity = 20
pick = pick_form.save()
pick.button_validate()
# Produces 3 products then creates a backorder for the remaining product.
@ -136,13 +164,13 @@ class TestMrpStockReports(TestReportsCommon):
backorder = backorder_form.save()
backorder.action_backorder()
mo_2 = (mo_1.procurement_group_id.mrp_production_ids - mo_1)
mo_2 = (mo_1.production_group_id.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)
self.assertEqual(len(lines), 2, "Must have 2 lines, one about the backorder")
self.assertEqual(lines[1]['document_in']['id'], mo_2.id)
self.assertEqual(lines[1]['quantity'], 1)
self.assertEqual(lines[1]['document_out'], False)
# Produces the last unit.
mo_form = Form(mo_2)
@ -151,14 +179,14 @@ class TestMrpStockReports(TestReportsCommon):
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")
self.assertEqual(len(lines), 1, "Free Stock 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',
'is_storable': True,
})
product_chocolate = self.env['product.product'].create({
'name': 'Chocolate',
@ -180,12 +208,62 @@ class TestMrpStockReports(TestReportsCommon):
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:
for line in lines[1:]:
if line['document_in']['id'] == mo.id:
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_kit_packaging_delivery_slip(self):
superkit = self.env['product.product'].create({
'name': 'Super Kit',
'type': 'consu',
'uom_id': self.env['uom.uom'].create({
'name': '6-pack',
'relative_factor': 6,
'relative_uom_id': self.env.ref('uom.product_uom_unit').id,
}).id,
})
compo01, compo02 = self.env['product.product'].create([{
'name': n,
'is_storable': True,
'uom_id': self.env.ref('uom.product_uom_meter').id,
} for n in ['Compo 01', 'Compo 02']])
self.env['mrp.bom'].create({
'product_tmpl_id': superkit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'product_uom_id': superkit.uom_id.id,
'bom_line_ids': [
(0, 0, {'product_id': compo01.id, 'product_qty': 6}),
(0, 0, {'product_id': compo02.id, 'product_qty': 6}),
],
})
for back_order, expected_vals in [('never', [12, 12]), ('always', [24, 12])]:
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.picking_type_in
picking_form.partner_id = self.partner
with picking_form.move_ids.new() as move:
move.product_id = superkit
move.product_uom_qty = 4
picking = picking_form.save()
picking.action_confirm()
picking.move_ids.write({'quantity': 12, 'picked': True})
picking.picking_type_id.create_backorder = back_order
picking.button_validate()
non_kit_aggregate_values = picking.move_line_ids._get_aggregated_product_quantities()
self.assertFalse(non_kit_aggregate_values)
aggregate_values = picking.move_line_ids._get_aggregated_product_quantities(kit_name=superkit.display_name)
for line in aggregate_values.values():
self.assertItemsEqual([line[val] for val in ['qty_ordered', 'quantity']], expected_vals)
html_report = self.env['ir.actions.report']._render_qweb_html('stock.report_deliveryslip', picking.ids)[0]
self.assertTrue(html_report, "report generated successfully")
def test_subkit_in_delivery_slip(self):
"""
Suppose this structure:
@ -222,24 +300,24 @@ class TestMrpStockReports(TestReportsCommon):
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:
with picking_form.move_ids.new() as move:
move.product_id = superkit
move.product_uom_qty = 1
with picking_form.move_ids_without_package.new() as move:
with picking_form.move_ids.new() as move:
move.product_id = subkit
move.product_uom_qty = 1
with picking_form.move_ids_without_package.new() as move:
with picking_form.move_ids.new() as move:
move.product_id = compo01
move.product_uom_qty = 1
with picking_form.move_ids_without_package.new() as move:
with picking_form.move_ids.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.move_ids.write({'quantity': 1, 'picked': True})
move = picking.move_ids.filtered(lambda m: m.description_picking == "Super Kit" and m.product_id == compo03)
move.move_line_ids.result_package_id = self.env['stock.package'].create({'name': 'Package0001'})
picking.button_validate()
html_report = self.env['ir.actions.report']._render_qweb_html(
@ -255,4 +333,409 @@ class TestMrpStockReports(TestReportsCommon):
break
if keys[0] in line:
keys = keys[1:]
self.assertFalse(keys, "All keys should be in the report with the defined order")
def test_mo_overview(self):
""" Test that the overview does not traceback when the final produced qty is 0
"""
product_chocolate = self.env['product.product'].create({
'name': 'Chocolate',
'type': 'consu',
})
product_chococake = self.env['product.product'].create({
'name': 'Choco Cake',
'is_storable': True,
})
workcenter = self.env['mrp.workcenter'].create({
'name': 'workcenter test',
'costs_hour': 10,
'time_start': 10,
'time_stop': 10,
'time_efficiency': 90,
})
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}),
],
'operation_ids': [
Command.create({
'name': 'Cutting Machine',
'workcenter_id': workcenter.id,
'time_cycle': 60,
'sequence': 1
}),
],
})
mo = self.env['mrp.production'].create({
'name': 'MO',
'product_qty': 1.0,
'product_id': product_chococake.id,
})
mo.action_confirm()
# check that the mo and bom cost are correctly calculated after mo confirmation
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
self.assertEqual(round(overview_values['data']['operations']['summary']['mo_cost'], 2), 14.45)
self.assertEqual(round(overview_values['data']['operations']['summary']['bom_cost'], 2), 14.44)
mo.button_mark_done()
mo.qty_produced = 0.
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
self.assertEqual(overview_values['data']['id'], mo.id, "computing overview value should work")
def test_overview_with_component_also_as_byproduct(self):
""" Check that opening he overview of an MO for which the BoM contains an element as both component and byproduct
does not cause an infinite recursion.
"""
bom = self.env['mrp.bom'].create({
'product_tmpl_id': self.product.product_tmpl_id.id,
'product_qty': 1.0,
'bom_line_ids': [
Command.create({'product_id': self.product1.id, 'product_qty': 1.0}),
],
'byproduct_ids': [
Command.create({'product_id': self.product1.id, 'product_qty': 1.0})
]
})
mo = self.env['mrp.production'].create({
'product_id': self.product.id,
'bom_id': bom.id,
'product_qty': 1,
})
mo.action_confirm()
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
self.assertEqual(overview_values['data']['id'], mo.id, "Unexpected disparity between overview and MO data")
def test_multi_step_component_forecast_availability(self):
"""
Test that the component availability is correcly forecasted
in multi step manufacturing
"""
# Configures the warehouse.
warehouse = self.env.ref('stock.warehouse0')
warehouse.manufacture_steps = 'pbm_sam'
final_product, component = self.product, self.product1
bom = self.env['mrp.bom'].create({
'product_id': final_product.id,
'product_tmpl_id': final_product.product_tmpl_id.id,
'product_uom_id': final_product.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
Command.create({'product_id': component.id, 'product_qty': 10}),
],
})
# Creates a MO without any component in stock
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = bom
mo_form.product_qty = 2
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.components_availability, 'Not Available')
self.assertEqual(mo.move_raw_ids.forecast_availability, -20.0)
self.env['stock.quant']._update_available_quantity(component, warehouse.lot_stock_id, 100)
# change the qty_producing to force a recompute of the availability
with Form(mo) as mo_form:
mo_form.qty_producing = 2.0
self.assertEqual(mo.components_availability, 'Available')
def test_mo_overview_same_component(self):
"""
Test that for an mo for a product which has 2+ component lines for the same product,
if there is some quantity of the component reserved, we properly match replenishments with
components lines
"""
# BOM structure:
# 'finished', manufactured:
# - 1 'part'
# - 1 'part'
part, finished = self.env['product.product'].create([
{
'name': name,
'type': 'consu',
'is_storable': True,
} for name in ['Part', 'Finished']
])
self.env['mrp.bom'].create([
{
'product_id': finished.id,
'product_tmpl_id': finished.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
Command.create({'product_id': part.id, 'product_qty': 1.0})
] * 2,
}
])
# Put 2 parts in stock
self.env['stock.quant']._update_available_quantity(part, self.stock_location, 2)
# Receive 20 parts
self.env['stock.picking'].create({
'picking_type_id': self.picking_type_in.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'move_type': 'one',
'move_ids': [Command.create({
'product_id': part.id,
'product_uom_qty': 20,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.stock_location.id,
}),
],
}).action_confirm()
# Create an MO for 5 finished product
mo = self.env['mrp.production'].create({
'name': 'MO',
'product_qty': 5.0,
'product_id': finished.id,
})
mo.action_confirm()
# Test overview report values
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
[line0, line1] = overview_values['data']['components']
[repl0, repl1] = line0['replenishments'], line1['replenishments']
self.assertEqual(len(repl0), 1)
self.assertEqual(len(repl1), 1)
self.assertEqual(repl0[0]['summary']['quantity'], 3)
self.assertEqual(repl1[0]['summary']['quantity'], 5)
def test_report_price_variants(self):
"""
This tests the MO's report price when a variant is involved. It makes sure
that the BoM price takes only the current variant and not all of them. It also
tests that the lines that were removed from the MO but are still in the bom are
used in the BoM cost computing. Lastly, it makes sure that Kits are also accounted
for and used in the BoM cost as they should.
"""
# Create a color variant, which will be used to create a Product
attribute_color = self.env['product.attribute'].create({'name': 'Color'})
value_black, value_white = self.env['product.attribute.value'].create([
{
'name': 'Black',
'attribute_id': attribute_color.id,
},
{
'name': 'White',
'attribute_id': attribute_color.id,
},
])
# Create 3 product templates, one for the variants, to check to not all variants are used
# to compute the cost of the MO, another one to make sure the 'missing components' are still
# taken into account (they are the product that were removed from the MO but are still present
# in the BoM), and the last one to make sure that kits are still taken in as well.
product_variants, missing_product, kit_product = self.env['product.template'].create([
{
'name': 'Variant Product',
'type': 'consu',
'attribute_line_ids': [Command.create(
{
'attribute_id': attribute_color.id,
'value_ids': [
Command.link(value_black.id),
Command.link(value_white.id),
],
},
)],
},
{
'name': 'Missing Component',
'type': 'consu',
'standard_price': 40,
},
{
'name': 'Kit Product',
'type': 'consu',
'standard_price': 60,
},
])
variant_black = product_variants.product_variant_ids[0]
variant_white = product_variants.product_variant_ids[1]
variant_black.standard_price = 50
variant_white.standard_price = 30
bom_normal, _ = self.env['mrp.bom'].create([
{
'product_tmpl_id': product_variants.id,
'type': 'normal',
'bom_line_ids': [Command.create({
'product_id': missing_product.product_variant_id.id,
'product_qty': 1,
}),
Command.create({
'product_id': kit_product.product_variant_id.id,
'product_qty': 1,
})],
},
{
'product_tmpl_id': kit_product.id,
'type': 'phantom',
'bom_line_ids': [Command.create({
'product_id': missing_product.product_variant_id.id,
'product_qty': 1,
})]
},
])
white_tmpl = bom_normal.product_tmpl_id.product_variant_ids.product_template_attribute_value_ids.filtered(lambda tmpl: tmpl.product_attribute_value_id == value_white)
black_tmpl = bom_normal.product_tmpl_id.product_variant_ids.product_template_attribute_value_ids.filtered(lambda tmpl: tmpl.product_attribute_value_id == value_black)
black_white_product, black_product, white_product = self.env['product.product'].create([{
'name': 'Black White',
'type': 'consu',
'standard_price': 25,
},
{
'name': 'Black Only',
'type': 'consu',
'standard_price': 15,
},
{
'name': 'White Only',
'type': 'consu',
'standard_price': 10,
},
])
self.env['mrp.bom.line'].create([{
'bom_id': bom_normal.id,
'product_id': black_white_product.id,
'product_qty': 1,
'bom_product_template_attribute_value_ids': [Command.set([black_tmpl.id, white_tmpl.id])],
},
{
'bom_id': bom_normal.id,
'product_id': black_product.id,
'product_qty': 1,
'bom_product_template_attribute_value_ids': [Command.set([black_tmpl.id])],
},
{
'bom_id': bom_normal.id,
'product_id': white_product.id,
'product_qty': 1,
'bom_product_template_attribute_value_ids': [Command.set([white_tmpl.id])],
}
])
mo = self.env['mrp.production'].create({
'product_id': variant_black.id,
'product_qty': 1,
'bom_id': bom_normal.id,
})
mo_report = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
self.assertEqual(mo_report['data']['extras']['unit_bom_cost'], mo_report['data']['extras']['unit_mo_cost'], 'The BoM unit cost should be equal to the sum of the products of said BoM')
# Check that the missing components (the components that were removed from the MO but are still in the BoM)
# are taken into account when computing the BoM cost
mo.move_raw_ids.filtered(lambda m: m.product_id == missing_product.product_variant_id and m.bom_line_id.bom_id.type != 'phantom').unlink()
# When a product has two possible variants, and then is deleted, it should be taken in the missing components
mo.move_raw_ids.filtered(lambda m: m.product_id == black_white_product and m.bom_line_id.bom_id.type != 'phantom').unlink()
mo_report = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
self.assertEqual(mo_report['data']['extras']['unit_bom_cost'], mo_report['data']['extras']['unit_mo_cost'] + missing_product.standard_price + black_white_product.standard_price, 'The BoM unit cost should take the missing components into account, which are the deleted MO lines')
def test_mo_overview_operation_cost(self):
""" Test that operations correctly compute their cost depending on their cost_mode. """
expedition_33 = self.product
lumiere = self.env['mrp.workcenter'].create({
'name': 'Lumière',
'costs_hour': 33,
})
bom_baguette = self.env['mrp.bom'].create({
'product_id': expedition_33.id,
'product_tmpl_id': expedition_33.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'operation_ids': [
Command.create({
'name': 'Get on a boat',
'workcenter_id': lumiere.id,
'time_cycle': 60,
'sequence': 1,
'cost_mode': 'actual'
}),
Command.create({
'name': 'Die on a beach',
'workcenter_id': lumiere.id,
'time_cycle': 60,
'sequence': 2,
'cost_mode': 'estimated'
}),
],
})
mo = self.env['mrp.production'].create({
'name': 'MO',
'product_qty': 1.0,
'product_id': expedition_33.id,
})
mo.action_confirm()
mo.workorder_ids.duration = 10
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
self.assertEqual(overview_values['data']['operations']['details'][0]['mo_cost'], 33.0)
self.assertEqual(overview_values['data']['operations']['details'][0]['real_cost'], 5.5)
self.assertEqual(overview_values['data']['operations']['details'][1]['mo_cost'], 33.0)
self.assertEqual(overview_values['data']['operations']['details'][1]['real_cost'], 33.0)
# Costs should stay the same for a done MO and/or if the cost_mode of the operation is changed
mo.button_mark_done()
bom_baguette.operation_ids.filtered(lambda o: o.cost_mode == 'estimated').cost_mode = 'actual'
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
self.assertEqual(overview_values['data']['operations']['details'][0]['mo_cost'], 33.0)
self.assertEqual(overview_values['data']['operations']['details'][0]['real_cost'], 5.5)
self.assertEqual(overview_values['data']['operations']['details'][1]['mo_cost'], 33.0)
self.assertEqual(overview_values['data']['operations']['details'][1]['real_cost'], 33.0)
def test_mo_overview_with_different_uom(self):
"""Ensure that the MO overview correctly computes costs
when the product UoM differs from the BoM UoM.
In this case, the product is defined in Unit while the BoM
is defined in Dozen.
"""
self.env['mrp.bom'].create({
'product_id': self.product.id,
'product_tmpl_id': self.product.product_tmpl_id.id,
'product_uom_id': self.env.ref('uom.product_uom_dozen').id,
'product_qty': 1.0,
'bom_line_ids': [Command.create({
'product_id': self.product1.id,
'product_qty': 12.0,
})],
})
self.product1.standard_price = 10
# create MO for 1 dozen of the product
mo = self.env['mrp.production'].create({
'name': 'MO',
'bom_id': self.product.bom_ids.id
})
mo.action_confirm()
# check that the mo and bom cost are correctly calculated after mo confirmation
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
self.assertEqual(overview_values['data']['components'][0]['summary']['bom_cost'], 120)
self.assertEqual(overview_values['data']['components'][0]['summary']['mo_cost'], 120)
# Test without BoM
mo_no_bom = self.env['mrp.production'].create({
'name': 'MO without BoM',
'product_id': self.product.id,
'product_uom_id': self.env.ref('uom.product_uom_dozen').id,
'product_qty': 1.0,
'bom_id': False,
'move_raw_ids': [Command.create({
'product_id': self.product1.id,
'product_uom_qty': 12.0,
})],
})
mo_no_bom.action_confirm()
overview_values_no_bom = self.env['report.mrp.report_mo_overview'].get_report_values(mo_no_bom.id)
self.assertEqual(overview_values_no_bom['data']['components'][0]['summary']['bom_cost'], 120)
self.assertEqual(overview_values_no_bom['data']['components'][0]['summary']['mo_cost'], 120)

View file

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

View file

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

View file

@ -12,15 +12,19 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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')
cls.env.user.group_ids += cls.env.ref('uom.group_uom')
cls.env.user.group_ids += cls.env.ref('product.group_product_variant')
# Required for `manufacture_steps` to be visible in the view
cls.env.user.groups_id += cls.env.ref('stock.group_adv_location')
cls.env.user.group_ids += cls.env.ref('stock.group_adv_location')
# Required for `product_id` to be visible in the view
cls.env.user.group_ids += cls.env.ref('product.group_product_variant')
# 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()
# Enable MTO
cls.warehouse.mto_pull_id.route_id.active = True
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
@ -28,19 +32,16 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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.is_storable = True
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.is_storable = True
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
@ -85,12 +86,53 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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 """
""" Warehouse testing for picking and 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)
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.lot_stock_id.id)
def test_manufacturing_2_steps_sublocation(self):
"""Check having a production order taking stock in a child location of pre prod
create correctly a 2 steps manufacturing even with only one mto rule from pre-pro to
production. """
self.warehouse.manufacture_steps = 'pbm'
pre_1, pre_2 = self.env['stock.location'].create([{
'name': name,
'location_id': self.warehouse.pbm_loc_id.id,
'usage': 'internal'
} for name in ('Pre 1', 'Pre 2')])
# create 2 picking type having 2 different pre-prod location
pick_1 = self.warehouse.manu_type_id.copy({
'sequence_code': 'PRE1',
'default_location_src_id': pre_1.id,
})
pick_2 = self.warehouse.manu_type_id.copy({
'sequence_code': 'PRE2',
'default_location_src_id': pre_2.id,
})
production_form = Form(self.env['mrp.production'])
production_form.picking_type_id = pick_1
production_form.product_id = self.finished_product
production = production_form.save()
production.action_confirm()
# check that picking is created
pick = production.picking_ids
self.assertEqual(pick.location_id, self.warehouse.lot_stock_id)
self.assertEqual(pick.location_dest_id, pre_1)
production_form = Form(self.env['mrp.production'])
production_form.picking_type_id = pick_2
production_form.product_id = self.finished_product
production = production_form.save()
production.action_confirm()
# check that picking is created
pick = production.picking_ids
self.assertEqual(pick.location_id, self.warehouse.lot_stock_id)
self.assertEqual(pick.location_dest_id, pre_2)
def test_manufacturing_3_steps(self):
""" Test MO/picking before manufacturing/picking after manufacturing
@ -121,6 +163,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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)
production.button_mark_done()
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)
@ -137,24 +180,24 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
"""
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.route_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,
'location_dest_id': self.customer_location.id,
'partner_id': self.env['ir.model.data']._xmlid_to_res_id('base.res_partner_4'),
'picking_type_id': self.warehouse.out_type_id.id,
'state': 'draft',
})
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,
'location_id': self.warehouse.lot_stock_id.id,
'location_dest_id': self.customer_location.id,
'procure_method': 'make_to_order',
'origin': 'SOURCEDOCUMENT',
'state': 'draft',
@ -179,14 +222,12 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
]).picking_id
self.assertTrue(picking_stock_preprod)
self.assertTrue(picking_stock_postprod)
self.assertFalse(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.move_ids.write({'quantity': 4, 'picked': True})
picking_stock_preprod._action_done()
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.lot_stock_id).mapped('quantity')))
@ -194,7 +235,6 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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
@ -203,15 +243,16 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id).mapped('quantity')))
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_postprod)
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)
self.assertEqual(picking_customer.move_ids.move_orig_ids.picking_id, picking_stock_postprod)
def test_cancel_propagation(self):
""" Test cancelling moves in a 'picking before
@ -223,18 +264,18 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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,
'location_dest_id': self.customer_location.id,
'partner_id': self.env['ir.model.data']._xmlid_to_res_id('base.res_partner_4'),
'picking_type_id': self.warehouse.out_type_id.id,
'state': 'draft',
})
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,
'location_dest_id': self.customer_location.id,
'procure_method': 'make_to_order',
})
picking_customer.action_confirm()
@ -247,22 +288,13 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
('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
@ -294,7 +326,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
])
new_product = self.env['product.product'].create({
'name': 'New product',
'type': 'product',
'is_storable': True,
})
bom.consumption = 'flexible'
production_form = Form(self.env['mrp.production'])
@ -332,19 +364,17 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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,
'relative_factor': i,
'relative_uom_id': one_unit_uom.id,
} 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',
'is_storable': True,
})
secondary_product = self.env['product.product'].create({
'name': 'Secondary',
'type': 'product',
'is_storable': True,
})
component = self.env['product.product'].create({
'name': 'Component',
@ -375,15 +405,18 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
'product_max_qty': 2,
})
self.env['procurement.group'].run_scheduler()
self.env['stock.rule'].run_scheduler()
mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
pickings = mo.picking_ids
self.assertEqual(len(pickings), 2)
self.assertEqual(len(pickings), 1)
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)
mo.button_mark_done()
pickings = mo.picking_ids
self.assertEqual(len(pickings), 2)
postprod_picking = pickings - preprod_picking
self.assertEqual(postprod_picking.location_id, post_production_location)
self.assertEqual(postprod_picking.location_dest_id, warehouse_stock_location)
@ -393,23 +426,18 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
('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)
self.assertEqual(byproduct_postprod_move.state, 'assigned')
self.assertEqual(byproduct_postprod_move.reference_ids.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.is_storable = True
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
@ -435,19 +463,19 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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.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()
rr_raw = rr_form.save()
self.env['procurement.group'].run_scheduler()
self.env['stock.rule'].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)
self.assertTrue(rr_raw.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
@ -466,7 +494,6 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
'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,
@ -478,114 +505,6 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
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 """
@ -599,7 +518,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
rr_form.product_max_qty = 40
rr_form.save()
self.env['procurement.group'].run_scheduler()
self.env['stock.rule'].run_scheduler()
mo = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
mo_form = Form(mo)
@ -667,7 +586,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
warehouse.manufacture_steps = 'pbm_sam'
finished_product = self.env['product.product'].create({
'name': 'Product',
'type': 'product',
'is_storable': True,
'route_ids': manufacturing_route,
})
self.env['mrp.bom'].create({
@ -690,8 +609,286 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
'route_id': manufacturing_route.id,
'bom_id': bom_2.id,
})
self.env['procurement.group'].run_scheduler()
self.env['stock.rule'].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)
# def test_manufacturing_bom_with_repetitions(self):
# """
# Checks that manufacturing orders created to manufacture the components of a BOM
# are set with the correct quantities when products appear with repetitions.
# - Create 5 products: product 1,2,3,4 (P1,P2,P3 and P4) and a final product (FP)
# - Set routes to manifacture on each product
# - For P1, P2, P3, P4 add a 0:0 reordering rule.
# - Add a BOM for P2 with 1 unit of P1 as components
# - Add a BOM for P3 with 1 unit of P2 as components
# - Add a BOM for P4 with 1 unit of P3 as components
# - Add a BOM for FP with 3 unit of P4 and 2 units of P3 as components
# """
# manufacturing_route = self.env['stock.rule'].search([
# ('action', '=', 'manufacture')]).route_id
# products = self.env['product.product'].create([
# {
# 'name': 'FP',
# 'is_storable': True,
# 'route_ids': manufacturing_route,
# },
# {
# 'name': 'P1',
# 'is_storable': True,
# 'route_ids': manufacturing_route,
# },
# {
# 'name': 'P2',
# 'is_storable': True,
# 'route_ids': manufacturing_route,
# },
# {
# 'name': 'P3',
# 'is_storable': True,
# 'route_ids': manufacturing_route,
# },
# {
# 'name': 'P4',
# 'is_storable': True,
# 'route_ids': manufacturing_route,
# },
#
# ])
# self.env['stock.warehouse.orderpoint'].create([
# {
# 'name': 'My orderpoint',
# 'product_id': i,
# 'product_min_qty': 0,
# 'product_max_qty': 0,
# } for i in products.ids[1:]
# ])
# self.env['mrp.bom'].create([
# {
# 'product_tmpl_id': products[2].product_tmpl_id.id,
# 'product_qty': 1,
# 'product_uom_id': products[2].uom_id.id,
# 'type': 'normal',
# 'bom_line_ids': [
# Command.create({
# 'product_id': products[1].id,
# 'product_qty': 1,
# })
# ]},
# {
# 'product_tmpl_id': products[3].product_tmpl_id.id,
# 'product_qty': 1,
# 'product_uom_id': products[3].uom_id.id,
# 'type': 'normal',
# 'bom_line_ids': [
# Command.create({
# 'product_id': products[2].id,
# 'product_qty': 1,
# })
# ]},
# {
# 'product_tmpl_id': products[4].product_tmpl_id.id,
# 'product_qty': 1,
# 'product_uom_id': products[4].uom_id.id,
# 'type': 'normal',
# 'bom_line_ids': [
# Command.create({
# 'product_id': products[3].id,
# 'product_qty': 1,
# })
# ]},
# {
# 'product_tmpl_id': products[0].product_tmpl_id.id,
# 'product_qty': 1,
# 'product_uom_id': products[0].uom_id.id,
# 'type': 'normal',
# 'bom_line_ids': [
# Command.create({
# 'product_id': products[4].id,
# 'product_qty': 3,
# }),
# Command.create({
# 'product_id': products[2].id,
# 'product_qty': 2,
# }),
# ]},
# ])
# mo = self.env['mrp.production'].create({
# 'product_id': products[0].id,
# 'product_uom_qty': 1,
# })
# mo.action_confirm()
# mo_P1 = self.env['mrp.production'].search([('product_id', '=', products[1].id)])
# mo_P2 = self.env['mrp.production'].search([('product_id', '=', products[2].id)])
# self.assertEqual(mo_P1.product_uom_qty, 5.0)
# self.assertEqual(mo_P2.product_uom_qty, 5.0)
#
def test_update_component_qty(self):
self.warehouse.manufacture_steps = "pbm"
component = self.bom.bom_line_ids.product_id
mo = self.env['mrp.production'].create({
'product_id': self.bom.product_id.id,
'bom_id': self.bom.id,
'product_qty': 1,
'location_src_id': self.warehouse.pbm_loc_id.id,
})
mo.action_confirm()
self.assertEqual(mo.move_raw_ids.product_uom_qty, 2.0)
self.assertEqual(mo.picking_ids.move_ids.product_uom_qty, 2.0)
# we require a more components to complete the MO
mo_form = Form(mo)
with mo_form.move_raw_ids.new() as raw_move:
raw_move.product_id = component
raw_move.product_uom_qty = 1.0
mo = mo_form.save()
# check that the related moves qty is correctly updated
self.assertEqual(mo.move_raw_ids.product_uom_qty, 3.0)
self.assertEqual(mo.picking_ids.move_ids.product_uom_qty, 3.0)
def test_component_and_byproduct_on_transfers(self):
"""
Checks if transfers is updated when we adding a new byproduct/component
after confirm the MO
"""
self.env.user.group_ids += self.env.ref('mrp.group_mrp_byproducts')
demo = self.env['product.product'].create({
'name': 'DEMO',
'is_storable': True,
})
comp1 = self.env['product.product'].create({
'name': 'COMP1'
})
comp2 = self.env['product.product'].create({
'name': 'COMP2'
})
bprod1 = self.env['product.product'].create({
'name': 'BPROD1'
})
bprod2 = self.env['product.product'].create({
'name': 'BPROD2'
})
warehouse = self.warehouse
warehouse.manufacture_steps = 'pbm_sam'
warehouse_stock_location = warehouse.lot_stock_id
self.env['mrp.bom'].create({
'product_tmpl_id': demo.product_tmpl_id.id,
'product_qty': 1,
'bom_line_ids': [(0, 0, {
'product_id': comp1.id,
'product_qty': 1,
})],
'byproduct_ids': [(0, 0, {
'product_id': bprod1.id,
'product_qty': 1,
})],
})
self.env['stock.warehouse.orderpoint'].create({
'warehouse_id': warehouse.id,
'location_id': warehouse_stock_location.id,
'product_id': demo.id,
'product_min_qty': 2,
'product_max_qty': 2,
})
self.env['stock.rule'].run_scheduler()
mo = self.env['mrp.production'].search([('product_id', '=', demo.id)])
mo.action_confirm()
mo_form = Form(mo)
with mo_form.move_raw_ids.new() as raw_move:
raw_move.product_id = comp2
raw_move.product_uom_qty = 1.0
with mo_form.move_byproduct_ids.new() as byprod_move:
byprod_move.product_id = bprod2
byprod_move.quantity = 1.0
mo = mo_form.save()
mo.with_context({'skip_consumption': True}).button_mark_done()
self.assertEqual(len(mo.picking_ids), 2, "Should have 2 pickings: Components + (Final product and byproducts)")
for picking in mo.picking_ids:
if demo in [m.product_id for m in picking.move_ids]:
self.assertEqual(len(picking.move_ids), 3, "Should have 3 moves for: Demo, Bprod1 and Bprod2")
self.assertEqual([move.product_id for move in picking.move_ids.sorted(lambda m: bool(m.product_id))], [demo, bprod1, bprod2])
else:
self.assertEqual(len(picking.move_ids), 2, "Should have 2 moves for: Comp1 and Comp2")
self.assertEqual([move.product_id for move in picking.move_ids.sorted(lambda m: bool(m.product_id))], [comp1, comp2])
def test_pick_components_uses_shipping_policy_from_picking_type(self):
self.warehouse.manufacture_steps = "pbm"
pick_components_type = self.warehouse.pbm_type_id
for move_type in ["direct", "one"]:
pick_components_type.move_type = move_type
mo = self.env["mrp.production"].create({
"bom_id": self.bom.id,
"location_src_id": self.warehouse.pbm_loc_id.id,
})
mo.action_confirm()
self.assertEqual(mo.picking_ids[0].picking_type_id, pick_components_type)
self.assertEqual(mo.picking_ids[0].move_type, move_type)
def test_update_mo_from_bom_forecast(self):
self.warehouse_1.manufacture_steps = 'pbm_sam'
self.env['stock.quant']._update_available_quantity(self.product_2, self.warehouse_1.lot_stock_id, 10)
self.env['stock.quant']._update_available_quantity(self.product_3, self.warehouse_1.lot_stock_id, 20)
mo = self.env['mrp.production'].create({
'bom_id': self.bom_1.id,
'picking_type_id': self.warehouse_1.manu_type_id.id,
})
mo.action_confirm()
self.assertEqual(self.product_1.virtual_available, -4)
self.assertEqual(self.product_2.virtual_available, 8)
rr = self.env['stock.warehouse.orderpoint'].create({
'name': 'John Cutter RR',
'product_id': self.product_1.id,
'warehouse_id': mo.warehouse_id.id,
})
self.assertEqual(rr.qty_forecast, -4)
# Update the BoM
self.bom_1.bom_line_ids[1].unlink()
self.bom_1.bom_line_ids[0].product_qty = 3
self.env['mrp.bom.line'].create({
'product_id': self.product_3.id,
'product_qty': 5,
'bom_id': self.bom_1.id,
})
mo.action_update_bom()
self.assertEqual(rr.qty_forecast, 0)
self.assertEqual(self.product_1.virtual_available, 0)
self.assertEqual(self.product_2.virtual_available, 7)
self.assertEqual(self.product_3.virtual_available, 15)
pre_prod_pick = mo.picking_ids.filtered(lambda p: p.picking_type_id == mo.warehouse_id.pbm_type_id)
self.assertRecordValues(pre_prod_pick.move_ids, [
{'product_id': self.product_2.id, 'product_uom_qty': 3, 'product_qty_available': 10},
{'product_id': self.product_1.id, 'product_uom_qty': 0, 'product_qty_available': 0},
{'product_id': self.product_3.id, 'product_uom_qty': 5, 'product_qty_available': 20},
])
def test_3_steps_manufacturing_forecast(self):
"""Check that a confirmed MO influence the forecast of the warehouse stock"""
self.warehouse_1.manufacture_steps = 'pbm_sam'
lovely_product = self.bom_1.product_id.copy({'uom_id': self.uom_unit.id})
self.bom_1.product_id = lovely_product
self.assertEqual(lovely_product.with_context(location_id=self.warehouse_1.lot_stock_id.id).virtual_available, 0.0)
mo = self.env['mrp.production'].create({
'bom_id': self.bom_1.id,
'picking_type_id': self.warehouse_1.manu_type_id.id,
'product_qty': 3.0,
})
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
self.assertEqual(lovely_product.with_context(location_id=self.warehouse_1.lot_stock_id.id).virtual_available, 3.0)
def test_manufacture_to_resupply_unchecks_and_unlinks_warehouse(self):
"""Unchecking Manufacture to Resupply should keep manufacture_to_resupply disabled."""
manufacture_route = self.warehouse.manufacture_pull_id.route_id
self.warehouse.manufacture_to_resupply = False
self.assertFalse(self.warehouse.manufacture_to_resupply)
self.assertNotIn(self.warehouse, manufacture_route.warehouse_ids)

View file

@ -0,0 +1,56 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta, datetime
from freezegun import freeze_time
from . import common
from odoo import Command
from odoo.tests import Form, tagged
@tagged('-at_install', 'post_install')
class TestWorkcenterOverview(common.TestMrpCommon):
@freeze_time('2020-03-13') # Friday
def test_workcenter_graph_data(self):
fake_bom = self.env['mrp.bom'].create({
'product_id': self.product_2.id,
'product_tmpl_id': self.product_2.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'consumption': 'flexible',
'operation_ids': [
Command.create({
'name': 'Make it look you are working',
'workcenter_id': self.workcenter_2.id,
'time_cycle_manual': 60, 'sequence': 1})
],
'type': 'normal',
})
lang = self.env['res.lang']._lang_get(self.env.user.lang)
lang.week_start = '3' # Wednesday
week_range, date_start, date_stop = self.workcenter_2._get_week_range_and_first_last_days()
self.assertEqual(next(iter(week_range)), date_start)
self.assertEqual(date_stop.strftime('%Y-%m-%d'), '2020-04-07')
self.assertEqual(list(week_range.items())[2][1], '18 - 24 Mar')
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_2
mo_form.bom_id = fake_bom
mo_form.product_qty = 20
mo_form.save().action_confirm()
mo_form_2 = Form(self.env['mrp.production'])
mo_form_2.product_id = self.product_2
mo_form_2.bom_id = fake_bom
mo_form_2.product_qty = 60
mo_form_2.date_start = datetime.today() + timedelta(weeks=1)
mo_form_2.save().action_confirm()
wc_load_data = self.workcenter_2._get_workcenter_load_per_week(week_range, date_start, date_stop)
self.assertListEqual(list(wc_load_data[self.workcenter_2].values()), [20.0, 60.0])
self.assertListEqual(list(wc_load_data[self.workcenter_2].keys()), [datetime(2020, 3, 11), datetime(2020, 3, 18)])
load_graph_data = self.workcenter_2._prepare_graph_data(wc_load_data, week_range)
self.assertEqual(load_graph_data[self.workcenter_2.id][0]['is_sample_data'], False)
self.assertListEqual(load_graph_data[self.workcenter_2.id][0]['labels'], list(week_range.values()))
self.assertListEqual(load_graph_data[self.workcenter_2.id][0]['values'], [[0, 20.0, 40.0, 0, 0], 40.0, [0, 0, 20.0, 0, 0]])