mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-21 18:32:07 +02:00
Initial commit: Mrp packages
This commit is contained in:
commit
50d736b3bd
739 changed files with 538193 additions and 0 deletions
19
odoo-bringout-oca-ocb-mrp/mrp/tests/__init__.py
Normal file
19
odoo-bringout-oca-ocb-mrp/mrp/tests/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_bom
|
||||
from . import test_byproduct
|
||||
from . import test_cancel_mo
|
||||
from . import test_order
|
||||
from . import test_stock
|
||||
from . import test_stock_report
|
||||
from . import test_warehouse_multistep_manufacturing
|
||||
from . import test_procurement
|
||||
from . import test_unbuild
|
||||
from . import test_oee
|
||||
from . import test_traceability
|
||||
from . import test_multicompany
|
||||
from . import test_backorder
|
||||
from . import test_smp
|
||||
from . import test_performance
|
||||
from . import test_consume_tracked_component
|
||||
from . import test_manual_consumption
|
||||
259
odoo-bringout-oca-ocb-mrp/mrp/tests/common.py
Normal file
259
odoo-bringout-oca-ocb-mrp/mrp/tests/common.py
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.stock.tests import common2
|
||||
|
||||
|
||||
class TestMrpCommon(common2.TestStockCommon):
|
||||
|
||||
@classmethod
|
||||
def generate_mo(cls, tracking_final='none', tracking_base_1='none', tracking_base_2='none', qty_final=5, qty_base_1=4, qty_base_2=1, picking_type_id=False, consumption=False):
|
||||
""" This function generate a manufacturing order with one final
|
||||
product and two consumed product. Arguments allows to choose
|
||||
the tracking/qty for each different products. It returns the
|
||||
MO, used bom and the tree products.
|
||||
"""
|
||||
product_to_build = cls.env['product.product'].create({
|
||||
'name': 'Young Tom',
|
||||
'type': 'product',
|
||||
'tracking': tracking_final,
|
||||
})
|
||||
product_to_use_1 = cls.env['product.product'].create({
|
||||
'name': 'Botox',
|
||||
'type': 'product',
|
||||
'tracking': tracking_base_1,
|
||||
})
|
||||
product_to_use_2 = cls.env['product.product'].create({
|
||||
'name': 'Old Tom',
|
||||
'type': 'product',
|
||||
'tracking': tracking_base_2,
|
||||
})
|
||||
bom_1 = cls.env['mrp.bom'].create({
|
||||
'product_id': product_to_build.id,
|
||||
'product_tmpl_id': product_to_build.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'consumption': consumption if consumption else 'flexible',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_to_use_2.id, 'product_qty': qty_base_2}),
|
||||
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': qty_base_1})
|
||||
]})
|
||||
mo_form = Form(cls.env['mrp.production'])
|
||||
mo_form.product_id = product_to_build
|
||||
if picking_type_id:
|
||||
mo_form.picking_type_id = picking_type_id
|
||||
mo_form.bom_id = bom_1
|
||||
mo_form.product_qty = qty_final
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
return mo, bom_1, product_to_build, product_to_use_1, product_to_use_2
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpCommon, cls).setUpClass()
|
||||
|
||||
(
|
||||
cls.product_4,
|
||||
cls.product_5,
|
||||
cls.product_6,
|
||||
cls.product_8,
|
||||
) = cls.env['product.product'].create([{
|
||||
'name': 'Stick', # product_4
|
||||
'uom_id': cls.uom_dozen.id,
|
||||
'uom_po_id': cls.uom_dozen.id,
|
||||
}, {
|
||||
'name': 'Stone Tools', # product_5
|
||||
}, {
|
||||
'name': 'Door', # product_6
|
||||
}, {
|
||||
'name': 'House', # product_8
|
||||
}])
|
||||
|
||||
# Update demo products
|
||||
(cls.product_2 | cls.product_3 | cls.product_4 | cls.product_5 | cls.product_6 | cls.product_7_3 | cls.product_8).write({
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
# User Data: mrp user and mrp manager
|
||||
cls.user_mrp_user = mail_new_test_user(
|
||||
cls.env,
|
||||
name='Hilda Ferachwal',
|
||||
login='hilda',
|
||||
email='h.h@example.com',
|
||||
notification_type='inbox',
|
||||
groups='mrp.group_mrp_user, stock.group_stock_user, mrp.group_mrp_byproducts, uom.group_uom',
|
||||
)
|
||||
cls.user_mrp_manager = mail_new_test_user(
|
||||
cls.env,
|
||||
name='Gary Youngwomen',
|
||||
login='gary',
|
||||
email='g.g@example.com',
|
||||
notification_type='inbox',
|
||||
groups='mrp.group_mrp_manager, stock.group_stock_user, mrp.group_mrp_byproducts, uom.group_uom',
|
||||
)
|
||||
# Required for `product_uom_id` to be visible in the view
|
||||
# This class is used by a lot of tests which sets `product_uom_id` on `mrp.production`
|
||||
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
|
||||
|
||||
cls.workcenter_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter',
|
||||
'default_capacity': 2,
|
||||
'time_start': 10,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
})
|
||||
cls.workcenter_2 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Simple Workcenter',
|
||||
'default_capacity': 1,
|
||||
'time_start': 0,
|
||||
'time_stop': 0,
|
||||
'time_efficiency': 100,
|
||||
})
|
||||
cls.workcenter_3 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Double Workcenter',
|
||||
'default_capacity': 2,
|
||||
'time_start': 0,
|
||||
'time_stop': 0,
|
||||
'time_efficiency': 100,
|
||||
})
|
||||
|
||||
cls.bom_1 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_4.id,
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
|
||||
]})
|
||||
cls.bom_2 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_5.id,
|
||||
'product_tmpl_id': cls.product_5.product_tmpl_id.id,
|
||||
'product_uom_id': cls.product_5.uom_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
'type': 'phantom',
|
||||
'sequence': 2,
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_3.id, 'product_qty': 3})
|
||||
]})
|
||||
cls.bom_3 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_dozen.id,
|
||||
'ready_to_produce': 'asap',
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 2.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
(0, 0, {'name': 'Weld Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_5.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}),
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 12})
|
||||
]})
|
||||
cls.bom_4 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Rub it gently with a cloth', 'workcenter_id': cls.workcenter_2.id,
|
||||
'time_mode_batch': 1, 'time_mode': "auto", 'sequence': 1}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
]})
|
||||
cls.bom_5 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Rub it gently with a cloth two at once', 'workcenter_id': cls.workcenter_3.id,
|
||||
'time_mode_batch': 2, 'time_mode': "auto", 'sequence': 1}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
]})
|
||||
cls.bom_6 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Rub it gently with a cloth two at once', 'workcenter_id': cls.workcenter_3.id,
|
||||
'time_mode_batch': 1, 'time_mode': "auto", 'sequence': 1}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
cls.stock_location_14 = cls.env['stock.location'].create({
|
||||
'name': 'Shelf 2',
|
||||
'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
|
||||
})
|
||||
cls.stock_location_components = cls.env['stock.location'].create({
|
||||
'name': 'Shelf 1',
|
||||
'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
|
||||
})
|
||||
cls.laptop = cls.env['product.product'].create({
|
||||
'name': 'Acoustic Bloc Screens',
|
||||
'uom_id': cls.env.ref("uom.product_uom_unit").id,
|
||||
'uom_po_id': cls.env.ref("uom.product_uom_unit").id,
|
||||
'type': 'product',
|
||||
'tracking': 'none',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.graphics_card = cls.env['product.product'].create({
|
||||
'name': 'Individual Workplace',
|
||||
'uom_id': cls.env.ref("uom.product_uom_unit").id,
|
||||
'uom_po_id': cls.env.ref("uom.product_uom_unit").id,
|
||||
'type': 'product',
|
||||
'tracking': 'none',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def make_prods(cls, n):
|
||||
return [
|
||||
cls.env["product.product"].create(
|
||||
{"name": f"p{k + 1}", "type": "product"}
|
||||
)
|
||||
for k in range(n)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def make_bom(cls, p, *cs):
|
||||
return cls.env["mrp.bom"].create(
|
||||
{
|
||||
"product_tmpl_id": p.product_tmpl_id.id,
|
||||
"product_id": p.id,
|
||||
"product_qty": 1,
|
||||
"type": "phantom",
|
||||
"product_uom_id": cls.uom_unit.id,
|
||||
"bom_line_ids": [
|
||||
(0, 0, {
|
||||
"product_id": c.id,
|
||||
"product_qty": 1,
|
||||
"product_uom_id": cls.uom_unit.id
|
||||
})
|
||||
for c in cs
|
||||
],
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
import copy
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import common, Form
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
class TestConsumeTrackedComponentCommon(common.TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
The following variables are used in each test to define the number of MO to generate.
|
||||
They're also used as a verification in the executeConsumptionTriggers() to see if enough MO were passed to it
|
||||
in order to test all the triggers.
|
||||
|
||||
SERIAL : MO's product_tracking is 'serial'
|
||||
DEFAULT : MO's product_tracking is 'none' or 'lot'
|
||||
AVAILABLE : MO'S raw components are fully available
|
||||
"""
|
||||
super().setUpClass()
|
||||
|
||||
cls.SERIAL_AVAILABLE_TRIGGERS_COUNT = 3
|
||||
cls.DEFAULT_AVAILABLE_TRIGGERS_COUNT = 2
|
||||
cls.SERIAL_TRIGGERS_COUNT = 2
|
||||
cls.DEFAULT_TRIGGERS_COUNT = 1
|
||||
|
||||
cls.manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture')
|
||||
cls.stock_id = cls.env.ref('stock.stock_location_stock').id
|
||||
|
||||
cls.picking_type = cls.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
|
||||
cls.picking_type.use_create_components_lots = True
|
||||
cls.picking_type.use_auto_consume_components_lots = True
|
||||
|
||||
#Create Products & Components
|
||||
cls.produced_lot = cls.env['product.product'].create({
|
||||
'name': 'Produced Lot',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'lot',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
cls.produced_serial = cls.env['product.product'].create({
|
||||
'name': 'Produced Serial',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'serial',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
cls.produced_none = cls.env['product.product'].create({
|
||||
'name': 'Produced None',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'none',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
|
||||
cls.raw_lot = cls.env['product.product'].create({
|
||||
'name': 'Raw Lot',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'lot',
|
||||
})
|
||||
cls.raw_serial = cls.env['product.product'].create({
|
||||
'name': 'Raw Serial',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'serial',
|
||||
})
|
||||
cls.raw_none = cls.env['product.product'].create({
|
||||
'name': 'Raw None',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'none',
|
||||
})
|
||||
|
||||
cls.raws = [cls.raw_none, cls.raw_lot, cls.raw_serial]
|
||||
|
||||
#Workcenter
|
||||
cls.workcenter = cls.env['mrp.workcenter'].create({
|
||||
'name' : 'Assembly Line',
|
||||
})
|
||||
|
||||
#BoMs
|
||||
cls.bom_none = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id' : cls.produced_none.product_tmpl_id.id,
|
||||
'product_uom_id' : cls.produced_none.uom_id.id,
|
||||
'consumption' : 'flexible',
|
||||
'sequence' : 1
|
||||
})
|
||||
|
||||
cls.bom_none_lines = cls.create_bom_lines(cls.bom_none, cls.raws, [3, 2, 1])
|
||||
|
||||
cls.bom_lot = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id' : cls.produced_lot.product_tmpl_id.id,
|
||||
'product_uom_id' : cls.produced_lot.uom_id.id,
|
||||
'consumption' : 'flexible',
|
||||
'sequence' : 2
|
||||
})
|
||||
|
||||
cls.bom_lot_lines = cls.create_bom_lines(cls.bom_lot, cls.raws, [3, 2, 1])
|
||||
|
||||
cls.bom_serial = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id' : cls.produced_serial.product_tmpl_id.id,
|
||||
'product_uom_id' : cls.produced_serial.uom_id.id,
|
||||
'consumption' : 'flexible',
|
||||
'sequence' : 1
|
||||
})
|
||||
|
||||
cls.bom_serial_lines = cls.create_bom_lines(cls.bom_serial, cls.raws, [3, 2, 1])
|
||||
|
||||
#Manufacturing Orders
|
||||
cls.mo_none_tmpl = {
|
||||
'product_id' : cls.produced_none.id,
|
||||
'product_uom_id' : cls.produced_none.uom_id.id,
|
||||
'product_qty' : 1,
|
||||
'bom_id' : cls.bom_none.id
|
||||
}
|
||||
|
||||
cls.mo_lot_tmpl = {
|
||||
'product_id' : cls.produced_lot.id,
|
||||
'product_uom_id' : cls.produced_lot.uom_id.id,
|
||||
'product_qty' : 1,
|
||||
'bom_id' : cls.bom_lot.id
|
||||
}
|
||||
|
||||
cls.mo_serial_tmpl = {
|
||||
'product_id' : cls.produced_serial.id,
|
||||
'product_uom_id' : cls.produced_serial.uom_id.id,
|
||||
'product_qty' : 1,
|
||||
'bom_id' : cls.bom_serial.id
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_quant(cls, product, qty, offset=0, name="L"):
|
||||
i = 1
|
||||
if product.tracking == 'serial':
|
||||
i, qty = qty, 1
|
||||
if name == "L":
|
||||
name = "S"
|
||||
|
||||
vals = []
|
||||
for x in range(1, i+1):
|
||||
qDict = {
|
||||
'location_id': cls.stock_id,
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': qty,
|
||||
}
|
||||
|
||||
if product.tracking != 'none':
|
||||
qDict['lot_id'] = cls.env['stock.lot'].create({
|
||||
'name': name + str(offset + x),
|
||||
'product_id': product.id,
|
||||
'company_id': cls.env.company.id
|
||||
}).id
|
||||
vals.append(qDict)
|
||||
|
||||
return cls.env['stock.quant'].create(vals)
|
||||
|
||||
@classmethod
|
||||
def create_bom_lines(cls, bom, products, quantities=None):
|
||||
if quantities is None:
|
||||
quantities = [1 for i in range(len(products))]
|
||||
|
||||
vals = []
|
||||
for product, seq in zip(products, range(len(products))):
|
||||
vals.append({
|
||||
'product_id' : product.id,
|
||||
'product_qty' : quantities[seq],
|
||||
'product_uom_id' : product.uom_id.id,
|
||||
'sequence' : seq,
|
||||
'bom_id' : bom.id,
|
||||
})
|
||||
|
||||
return cls.env['mrp.bom.line'].create(vals)
|
||||
|
||||
@classmethod
|
||||
def create_mo(cls, template, count):
|
||||
vals = []
|
||||
for _ in range(count):
|
||||
vals.append(copy.deepcopy(template))
|
||||
return cls.env['mrp.production'].create(vals)
|
||||
|
||||
def executeConsumptionTriggers(self, mrp_productions):
|
||||
"""
|
||||
There's 3 different triggers to test : _onchange_producing(), action_generate_serial(), button_mark_done().
|
||||
|
||||
Depending on the tracking of the final product and the availability of the components,
|
||||
only a part of these 3 triggers is available or intended to work.
|
||||
|
||||
This function automatically call and process the appropriate triggers.
|
||||
"""
|
||||
tracking = mrp_productions[0].product_tracking
|
||||
sameTracking = True
|
||||
for mo in mrp_productions:
|
||||
sameTracking = sameTracking and mo.product_tracking == tracking
|
||||
self.assertTrue(sameTracking, "MOs passed to the executeConsumptionTriggers method shall have the same product_tracking")
|
||||
|
||||
isSerial = tracking == 'serial'
|
||||
isAvailable = all(move.state == 'assigned' for move in mrp_productions.move_raw_ids)
|
||||
|
||||
countOk = True
|
||||
length = len(mrp_productions)
|
||||
if isSerial:
|
||||
if isAvailable:
|
||||
countOk = length == self.SERIAL_AVAILABLE_TRIGGERS_COUNT
|
||||
else:
|
||||
countOk = length == self.SERIAL_TRIGGERS_COUNT
|
||||
else:
|
||||
if isAvailable:
|
||||
countOk = length == self.DEFAULT_AVAILABLE_TRIGGERS_COUNT
|
||||
else:
|
||||
countOk = length == self.DEFAULT_TRIGGERS_COUNT
|
||||
self.assertTrue(countOk, "The number of MOs passed to the executeConsumptionTriggers method does not match the associated TRIGGERS_COUNT")
|
||||
|
||||
mrp_productions[0].qty_producing = mrp_productions[0].product_qty
|
||||
mrp_productions[0]._onchange_producing()
|
||||
|
||||
i = 1
|
||||
if isSerial:
|
||||
mrp_productions[i].action_generate_serial()
|
||||
i += 1
|
||||
|
||||
if isAvailable:
|
||||
mark_done_action = mrp_productions[i].button_mark_done()
|
||||
immediate_production_wizard = Form(
|
||||
self.env['mrp.immediate.production']
|
||||
.with_context(**mark_done_action['context'])
|
||||
).save()
|
||||
error = False
|
||||
has_zero_tracked_component = not mrp_productions[i].picking_type_id.use_auto_consume_components_lots and \
|
||||
any(m.state not in ['done', 'cancel'] and m.has_tracking != 'none' and float_is_zero(m.quantity_done, m.product_uom.rounding) for m in mrp_productions[i].move_raw_ids)
|
||||
try:
|
||||
immediate_production_wizard.process()
|
||||
except UserError:
|
||||
error = True
|
||||
if has_zero_tracked_component:
|
||||
self.assertTrue(error, "Immediate Production Wizard shall raise an error.")
|
||||
else:
|
||||
self.assertFalse(error, "Immediate Production Wizard shall not raise an error.")
|
||||
674
odoo-bringout-oca-ocb-mrp/mrp/tests/test_backorder.py
Normal file
674
odoo-bringout-oca-ocb-mrp/mrp/tests/test_backorder.py
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import Form
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestMrpProductionBackorder(TestMrpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
warehouse_form = Form(cls.env['stock.warehouse'])
|
||||
warehouse_form.name = 'Test Warehouse'
|
||||
warehouse_form.code = 'TWH'
|
||||
cls.warehouse = warehouse_form.save()
|
||||
|
||||
def test_no_tracking_1(self):
|
||||
"""Create a MO for 4 product. Produce 4. The backorder button should
|
||||
not appear and hitting mark as done should not open the backorder wizard.
|
||||
The name of the MO should be MO/001.
|
||||
"""
|
||||
mo = self.generate_mo(qty_final=4)[0]
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 4
|
||||
mo = mo_form.save()
|
||||
|
||||
# No backorder is proposed
|
||||
self.assertTrue(mo.button_mark_done())
|
||||
self.assertEqual(mo._get_quantity_to_backorder(), 0)
|
||||
self.assertTrue("-001" not in mo.name)
|
||||
|
||||
def test_no_tracking_2(self):
|
||||
"""Create a MO for 4 product. Produce 1. The backorder button should
|
||||
appear and hitting mark as done should open the backorder wizard. In the backorder
|
||||
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
|
||||
created.
|
||||
The sequence of the first MO should be MO/001-01, the sequence of the second MO
|
||||
should be MO/001-02.
|
||||
Check that all MO are reachable through the procurement group.
|
||||
"""
|
||||
production, _, _, product_to_use_1, _ = self.generate_mo(qty_final=4, qty_base_1=3)
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reserve_visible, True)
|
||||
|
||||
# Make some stock and reserve
|
||||
for product in production.move_raw_ids.product_id:
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': 100,
|
||||
'location_id': production.location_src_id.id,
|
||||
})._apply_inventory()
|
||||
production.action_assign()
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reserve_visible, False)
|
||||
|
||||
mo_form = Form(production)
|
||||
mo_form.qty_producing = 1
|
||||
production = mo_form.save()
|
||||
|
||||
action = production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
# Two related MO to the procurement group
|
||||
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), 2)
|
||||
|
||||
# Check MO backorder
|
||||
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(mo_backorder.product_id.id, production.product_id.id)
|
||||
self.assertEqual(mo_backorder.product_qty, 3)
|
||||
self.assertEqual(sum(mo_backorder.move_raw_ids.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_uom_qty")), 9)
|
||||
self.assertEqual(mo_backorder.reserve_visible, False) # the reservation of the first MO should've been moved here
|
||||
|
||||
def test_no_tracking_pbm_1(self):
|
||||
"""Create a MO for 4 product. Produce 1. The backorder button should
|
||||
appear and hitting mark as done should open the backorder wizard. In the backorder
|
||||
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
|
||||
created.
|
||||
The sequence of the first MO should be MO/001-01, the sequence of the second MO
|
||||
should be MO/001-02.
|
||||
Check that all MO are reachable through the procurement group.
|
||||
"""
|
||||
# Required for `manufacture_steps` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref("stock.group_adv_location")
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm'
|
||||
|
||||
production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id)
|
||||
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 2)
|
||||
self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2})
|
||||
|
||||
pbm_move = move_raw_ids.move_orig_ids
|
||||
self.assertEqual(len(pbm_move), 2)
|
||||
self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
|
||||
self.assertFalse(pbm_move.move_orig_ids)
|
||||
|
||||
mo_form = Form(production)
|
||||
mo_form.qty_producing = 1
|
||||
production = mo_form.save()
|
||||
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
|
||||
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
|
||||
|
||||
action = production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(mo_backorder.delivery_count, 1)
|
||||
|
||||
pbm_move |= mo_backorder.move_raw_ids.move_orig_ids
|
||||
# Check that quantity is correct
|
||||
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
|
||||
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
|
||||
|
||||
self.assertFalse(pbm_move.move_orig_ids)
|
||||
|
||||
def test_no_tracking_pbm_sam_1(self):
|
||||
"""Create a MO for 4 product. Produce 1. The backorder button should
|
||||
appear and hitting mark as done should open the backorder wizard. In the backorder
|
||||
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
|
||||
created.
|
||||
The sequence of the first MO should be MO/001-01, the sequence of the second MO
|
||||
should be MO/001-02.
|
||||
Check that all MO are reachable through the procurement group.
|
||||
"""
|
||||
# Required for `manufacture_steps` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref("stock.group_adv_location")
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id)
|
||||
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 2)
|
||||
self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2})
|
||||
|
||||
pbm_move = move_raw_ids.move_orig_ids
|
||||
self.assertEqual(len(pbm_move), 2)
|
||||
self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
|
||||
self.assertFalse(pbm_move.move_orig_ids)
|
||||
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
|
||||
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
|
||||
|
||||
sam_move = production.move_finished_ids.move_dest_ids
|
||||
self.assertEqual(len(sam_move), 1)
|
||||
self.assertEqual(sam_move.product_id.id, product_to_build.id)
|
||||
self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
|
||||
|
||||
mo_form = Form(production)
|
||||
mo_form.qty_producing = 1
|
||||
production = mo_form.save()
|
||||
|
||||
action = production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(mo_backorder.delivery_count, 2)
|
||||
|
||||
pbm_move |= mo_backorder.move_raw_ids.move_orig_ids
|
||||
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
|
||||
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
|
||||
|
||||
sam_move |= mo_backorder.move_finished_ids.move_orig_ids
|
||||
self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
|
||||
|
||||
def test_tracking_backorder_series_lot_1(self):
|
||||
""" Create a MO of 4 tracked products. all component is tracked by lots
|
||||
Produce one by one with one bakorder for each until end.
|
||||
"""
|
||||
nb_product_todo = 4
|
||||
production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='lot', tracking_base_1='lot', tracking_base_2='lot')
|
||||
lot_final = self.env['stock.lot'].create({
|
||||
'name': 'lot_final',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_consumed_1',
|
||||
'product_id': p1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_consumed_2',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, nb_product_todo*4, lot_id=lot_1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, nb_product_todo, lot_id=lot_2)
|
||||
|
||||
production.action_assign()
|
||||
active_production = production
|
||||
for i in range(nb_product_todo):
|
||||
|
||||
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 4
|
||||
ml.lot_id = lot_1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.lot_id = lot_2
|
||||
details_operation_form.save()
|
||||
|
||||
production_form = Form(active_production)
|
||||
production_form.qty_producing = 1
|
||||
production_form.lot_producing_id = lot_final
|
||||
active_production = production_form.save()
|
||||
|
||||
active_production.button_mark_done()
|
||||
if i + 1 != nb_product_todo: # If last MO, don't make a backorder
|
||||
action = active_production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
active_production = active_production.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), nb_product_todo, f'You should have the {nb_product_todo} final product in stock')
|
||||
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo)
|
||||
|
||||
def test_tracking_backorder_series_lot_2(self):
|
||||
"""
|
||||
Create a MO with component tracked by lots. Produce a part of the demand
|
||||
by using some specific lots (not the ones suggested by the onchange).
|
||||
The components' reservation of the backorder should consider which lots
|
||||
have been consumed in the initial MO
|
||||
"""
|
||||
production, _, _, p1, p2 = self.generate_mo(tracking_base_2='lot')
|
||||
lot1, lot2 = self.env['stock.lot'].create([{
|
||||
'name': f'lot_consumed_{i}',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for i in range(2)])
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 20)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot2)
|
||||
|
||||
production.action_assign()
|
||||
|
||||
production_form = Form(production)
|
||||
production_form.qty_producing = 3
|
||||
|
||||
details_operation_form = Form(production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 4 * 3
|
||||
details_operation_form.save()
|
||||
|
||||
# Consume 1 Product from lot1 and 2 from lot 2
|
||||
p2_smls = production.move_raw_ids.filtered(lambda m: m.product_id == p2).move_line_ids
|
||||
self.assertEqual(len(p2_smls), 2, 'One for each lot')
|
||||
details_operation_form = Form(production.move_raw_ids.filtered(lambda m: m.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.lot_id = lot1
|
||||
with details_operation_form.move_line_ids.edit(1) as ml:
|
||||
ml.qty_done = 2
|
||||
ml.lot_id = lot2
|
||||
details_operation_form.save()
|
||||
|
||||
production = production_form.save()
|
||||
action = production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
p2_bo_mls = production.procurement_group_id.mrp_production_ids[-1].move_raw_ids.filtered(lambda m: m.product_id == p2).move_line_ids
|
||||
self.assertEqual(len(p2_bo_mls), 1)
|
||||
self.assertEqual(p2_bo_mls.lot_id, lot1)
|
||||
self.assertEqual(p2_bo_mls.reserved_qty, 2)
|
||||
|
||||
def test_uom_backorder(self):
|
||||
"""
|
||||
test backorder component UoM different from the bom's UoM
|
||||
"""
|
||||
product_finished = self.env['product.product'].create({
|
||||
'name': 'Young Tom',
|
||||
'type': 'product',
|
||||
})
|
||||
product_component = self.env['product.product'].create({
|
||||
'name': 'Botox',
|
||||
'type': 'product',
|
||||
'uom_id': self.env.ref('uom.product_uom_kgm').id,
|
||||
'uom_po_id': self.env.ref('uom.product_uom_kgm').id,
|
||||
})
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_finished
|
||||
mo_form.bom_id = self.env['mrp.bom'].create({
|
||||
'product_id': product_finished.id,
|
||||
'product_tmpl_id': product_finished.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': product_component.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id':self.env.ref('uom.product_uom_gram').id,
|
||||
}),]
|
||||
})
|
||||
mo_form.product_qty = 1000
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(product_component, self.stock_location, 1000)
|
||||
mo.action_assign()
|
||||
|
||||
production_form = Form(mo)
|
||||
production_form.qty_producing = 300
|
||||
mo = production_form.save()
|
||||
|
||||
action = mo.button_mark_done()
|
||||
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder_form.save().action_backorder()
|
||||
# 300 Grams consumed and 700 reserved
|
||||
self.assertAlmostEqual(self.env['stock.quant']._gather(product_component, self.stock_location).reserved_quantity, 0.7)
|
||||
|
||||
def test_rounding_backorder(self):
|
||||
"""test backorder component rounding doesn't introduce reservation issues"""
|
||||
production, _, _, p1, p2 = self.generate_mo(qty_final=5, qty_base_1=1, qty_base_2=1)
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 100)
|
||||
|
||||
production.action_assign()
|
||||
|
||||
production_form = Form(production)
|
||||
production_form.qty_producing = 3.1
|
||||
production = production_form.save()
|
||||
|
||||
details_operation_form = Form(production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 3.09
|
||||
|
||||
details_operation_form.save()
|
||||
|
||||
action = production.button_mark_done()
|
||||
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder_form.save().action_backorder()
|
||||
backorder = production.procurement_group_id.mrp_production_ids[-1]
|
||||
# 3.09 consumed and 1.9 reserved
|
||||
self.assertAlmostEqual(self.env['stock.quant']._gather(p1, self.stock_location).reserved_quantity, 1.9)
|
||||
self.assertAlmostEqual(backorder.move_raw_ids.filtered(lambda m: m.product_id == p1).move_line_ids.reserved_qty, 1.9)
|
||||
|
||||
# Make sure we don't have an unreserve errors
|
||||
backorder.do_unreserve()
|
||||
|
||||
def test_tracking_backorder_series_serial_1(self):
|
||||
""" Create a MO of 4 tracked products (serial) with pbm_sam.
|
||||
all component is tracked by serial
|
||||
Produce one by one with one bakorder for each until end.
|
||||
"""
|
||||
nb_product_todo = 4
|
||||
production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='serial', tracking_base_1='serial', tracking_base_2='serial', qty_base_1=1)
|
||||
serials_final, serials_p1, serials_p2 = [], [], []
|
||||
for i in range(nb_product_todo):
|
||||
serials_final.append(self.env['stock.lot'].create({
|
||||
'name': f'lot_final_{i}',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
}))
|
||||
serials_p1.append(self.env['stock.lot'].create({
|
||||
'name': f'lot_consumed_1_{i}',
|
||||
'product_id': p1.id,
|
||||
'company_id': self.env.company.id,
|
||||
}))
|
||||
serials_p2.append(self.env['stock.lot'].create({
|
||||
'name': f'lot_consumed_2_{i}',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
}))
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1, lot_id=serials_p1[-1])
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=serials_p2[-1])
|
||||
|
||||
production.action_assign()
|
||||
active_production = production
|
||||
for i in range(nb_product_todo):
|
||||
|
||||
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.lot_id = serials_p1[i]
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.lot_id = serials_p2[i]
|
||||
details_operation_form.save()
|
||||
|
||||
production_form = Form(active_production)
|
||||
production_form.qty_producing = 1
|
||||
production_form.lot_producing_id = serials_final[i]
|
||||
active_production = production_form.save()
|
||||
active_production.button_mark_done()
|
||||
if i + 1 != nb_product_todo: # If last MO, don't make a backorder
|
||||
action = active_production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
active_production = active_production.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), nb_product_todo, f'You should have the {nb_product_todo} final product in stock')
|
||||
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo)
|
||||
|
||||
def test_tracking_backorder_immediate_production_serial_1(self):
|
||||
""" Create a MO to build 2 of a SN tracked product.
|
||||
Build both the starting MO and its backorder as immediate productions
|
||||
(i.e. Mark As Done without setting SN/filling any quantities)
|
||||
"""
|
||||
mo, _, p_final, p1, p2 = self.generate_mo(qty_final=2, tracking_final='serial', qty_base_1=2, qty_base_2=2)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 2.0)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 2.0)
|
||||
mo.action_assign()
|
||||
res_dict = mo.button_mark_done()
|
||||
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
|
||||
immediate_wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
res_dict = immediate_wizard.process()
|
||||
self.assertEqual(res_dict.get('res_model'), 'mrp.production.backorder')
|
||||
backorder_wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context']))
|
||||
|
||||
# backorder should automatically open
|
||||
action = backorder_wizard.save().action_backorder()
|
||||
self.assertEqual(action.get('res_model'), 'mrp.production')
|
||||
backorder_mo_form = Form(self.env[action['res_model']].with_context(action['context']).browse(action['res_id']))
|
||||
backorder_mo = backorder_mo_form.save()
|
||||
res_dict = backorder_mo.button_mark_done()
|
||||
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
|
||||
immediate_wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
immediate_wizard.process()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, "Incorrect number of final product produced.")
|
||||
self.assertEqual(len(self.env['stock.lot'].search([('product_id', '=', p_final.id)])), 2, "Serial Numbers were not correctly produced.")
|
||||
|
||||
def test_backorder_name(self):
|
||||
def produce_one(mo):
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
return mo.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
default_picking_type_id = self.env['mrp.production']._get_default_picking_type_id(self.env.company.id)
|
||||
default_picking_type = self.env['stock.picking.type'].browse(default_picking_type_id)
|
||||
mo_sequence = default_picking_type.sequence_id
|
||||
mo_sequence.prefix = "WH-MO-"
|
||||
initial_mo_name = mo_sequence.prefix + str(mo_sequence.number_next_actual).zfill(mo_sequence.padding)
|
||||
production = self.generate_mo(qty_final=5)[0]
|
||||
self.assertEqual(production.name, initial_mo_name)
|
||||
|
||||
backorder = produce_one(production)
|
||||
self.assertEqual(production.name, initial_mo_name + "-001")
|
||||
self.assertEqual(backorder.name, initial_mo_name + "-002")
|
||||
|
||||
backorder.backorder_sequence = 998
|
||||
for seq in [998, 999, 1000]:
|
||||
new_backorder = produce_one(backorder)
|
||||
self.assertEqual(backorder.name, initial_mo_name + "-" + str(seq))
|
||||
self.assertEqual(new_backorder.name, initial_mo_name + "-" + str(seq + 1))
|
||||
backorder = new_backorder
|
||||
|
||||
def test_backorder_name_without_procurement_group(self):
|
||||
production = self.generate_mo(qty_final=5)[0]
|
||||
mo_form = Form(production)
|
||||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
|
||||
# Remove pg to trigger fallback on backorder name
|
||||
mo.procurement_group_id = False
|
||||
action = mo.button_mark_done()
|
||||
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder_form.save().action_backorder()
|
||||
|
||||
# The pg is back
|
||||
self.assertTrue(production.procurement_group_id)
|
||||
backorder_ids = production.procurement_group_id.mrp_production_ids[1]
|
||||
self.assertEqual(production.name.split('-')[0], backorder_ids.name.split('-')[0])
|
||||
self.assertEqual(int(production.name.split('-')[1]) + 1, int(backorder_ids.name.split('-')[1]))
|
||||
|
||||
def test_split_draft(self):
|
||||
""" test splitting a draft MO """
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_qty': 3,
|
||||
'bom_id': self.bom_1.id,
|
||||
})
|
||||
self.assertEqual(mo.state, 'draft')
|
||||
action = mo.action_split()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
wizard.counter = 3
|
||||
action = wizard.save().action_split()
|
||||
|
||||
def test_split_merge(self):
|
||||
# Change 'Units' rounding to 1 (integer only quantities)
|
||||
self.uom_unit.rounding = 1
|
||||
# Create a mo for 10 products
|
||||
mo, _, _, p1, p2 = self.generate_mo(qty_final=10)
|
||||
# Split in 3 parts
|
||||
action = mo.action_split()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
wizard.counter = 3
|
||||
action = wizard.save().action_split()
|
||||
# Should have 3 mos
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 3)
|
||||
mo1 = mo.procurement_group_id.mrp_production_ids[0]
|
||||
mo2 = mo.procurement_group_id.mrp_production_ids[1]
|
||||
mo3 = mo.procurement_group_id.mrp_production_ids[2]
|
||||
# Check quantities
|
||||
self.assertEqual(mo1.product_qty, 3)
|
||||
self.assertEqual(mo2.product_qty, 3)
|
||||
self.assertEqual(mo3.product_qty, 4)
|
||||
# Check raw movew quantities
|
||||
self.assertEqual(mo1.move_raw_ids.filtered(lambda m: m.product_id == p1).product_qty, 12)
|
||||
self.assertEqual(mo2.move_raw_ids.filtered(lambda m: m.product_id == p1).product_qty, 12)
|
||||
self.assertEqual(mo3.move_raw_ids.filtered(lambda m: m.product_id == p1).product_qty, 16)
|
||||
self.assertEqual(mo1.move_raw_ids.filtered(lambda m: m.product_id == p2).product_qty, 3)
|
||||
self.assertEqual(mo2.move_raw_ids.filtered(lambda m: m.product_id == p2).product_qty, 3)
|
||||
self.assertEqual(mo3.move_raw_ids.filtered(lambda m: m.product_id == p2).product_qty, 4)
|
||||
|
||||
# Merge them back
|
||||
expected_origin = ",".join([mo1.name, mo2.name, mo3.name])
|
||||
action = (mo1 + mo2 + mo3).action_merge()
|
||||
mo = self.env[action['res_model']].browse(action['res_id'])
|
||||
# Check origin & initial quantity
|
||||
self.assertEqual(mo.origin, expected_origin)
|
||||
self.assertEqual(mo.product_qty, 10)
|
||||
|
||||
def test_reservation_method_w_mo(self):
|
||||
""" Create a MO for 2 units, Produce 1 and create a backorder.
|
||||
The MO and the backorder should be assigned according to the reservation method
|
||||
defined in the default manufacturing operation type
|
||||
"""
|
||||
def create_mo(date_planned_start=False):
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.bom_1.product_id
|
||||
mo_form.bom_id = self.bom_1
|
||||
mo_form.product_qty = 2
|
||||
if date_planned_start:
|
||||
mo_form.date_planned_start = date_planned_start
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
return mo
|
||||
|
||||
def produce_one(mo):
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
return mo.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
# Make some stock and reserve
|
||||
for product in self.bom_1.bom_line_ids.product_id:
|
||||
product.type = 'product'
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': 100,
|
||||
'location_id': self.stock_location.id,
|
||||
})._apply_inventory()
|
||||
|
||||
default_picking_type_id = self.env['mrp.production']._get_default_picking_type_id(self.env.company.id)
|
||||
default_picking_type = self.env['stock.picking.type'].browse(default_picking_type_id)
|
||||
|
||||
# make sure generated MO will auto-assign
|
||||
default_picking_type.reservation_method = 'at_confirm'
|
||||
production = create_mo()
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reserve_visible, False)
|
||||
# check whether the backorder follows the same scenario as the original MO
|
||||
backorder = produce_one(production)
|
||||
self.assertEqual(backorder.state, 'confirmed')
|
||||
self.assertEqual(backorder.reserve_visible, False)
|
||||
|
||||
# make sure generated MO will does not auto-assign
|
||||
default_picking_type.reservation_method = 'manual'
|
||||
production = create_mo()
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reserve_visible, True)
|
||||
backorder = produce_one(production)
|
||||
self.assertEqual(backorder.state, 'confirmed')
|
||||
self.assertEqual(backorder.reserve_visible, True)
|
||||
|
||||
# make sure generated MO auto-assigns according to scheduled date
|
||||
default_picking_type.reservation_method = 'by_date'
|
||||
default_picking_type.reservation_days_before = 2
|
||||
# too early for scheduled date => don't auto-assign
|
||||
production = create_mo(datetime.now() + timedelta(days=10))
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reserve_visible, True)
|
||||
backorder = produce_one(production)
|
||||
self.assertEqual(backorder.state, 'confirmed')
|
||||
self.assertEqual(backorder.reserve_visible, True)
|
||||
|
||||
# within scheduled date + reservation days before => auto-assign
|
||||
production = create_mo()
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reserve_visible, False)
|
||||
backorder = produce_one(production)
|
||||
self.assertEqual(backorder.state, 'confirmed')
|
||||
self.assertEqual(backorder.reserve_visible, False)
|
||||
|
||||
def test_split_mo(self):
|
||||
"""
|
||||
Test that an MO is split correctly.
|
||||
BoM: 1 finished product = 0.5 comp1 + 1 comp2
|
||||
"""
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_qty': 10,
|
||||
'bom_id': self.bom_1.id,
|
||||
})
|
||||
self.assertEqual(mo.move_raw_ids.mapped('product_uom_qty'), [5, 10])
|
||||
self.assertEqual(mo.state, 'draft')
|
||||
action = mo.action_split()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
wizard.counter = 10
|
||||
action = wizard.save().action_split()
|
||||
# check that the MO is split in 10 and the components are split accordingly
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 10)
|
||||
self.assertEqual(mo.product_qty, 1)
|
||||
self.assertEqual(mo.move_raw_ids.mapped('product_uom_qty'), [0.5, 1])
|
||||
|
||||
|
||||
class TestMrpWorkorderBackorder(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpWorkorderBackorder, cls).setUpClass()
|
||||
cls.uom_unit = cls.env['uom.uom'].search([
|
||||
('category_id', '=', cls.env.ref('uom.product_uom_categ_unit').id),
|
||||
('uom_type', '=', 'reference')
|
||||
], limit=1)
|
||||
cls.finished1 = cls.env['product.product'].create({
|
||||
'name': 'finished1',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.compfinished1 = cls.env['product.product'].create({
|
||||
'name': 'compfinished1',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.compfinished2 = cls.env['product.product'].create({
|
||||
'name': 'compfinished2',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.workcenter1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'workcenter1',
|
||||
})
|
||||
cls.workcenter2 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'workcenter2',
|
||||
})
|
||||
|
||||
cls.bom_finished1 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.finished1.id,
|
||||
'product_tmpl_id': cls.finished1.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.compfinished1.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': cls.compfinished2.id, 'product_qty': 1}),
|
||||
],
|
||||
'operation_ids': [
|
||||
(0, 0, {'sequence': 1, 'name': 'finished operation 1', 'workcenter_id': cls.workcenter1.id}),
|
||||
(0, 0, {'sequence': 2, 'name': 'finished operation 2', 'workcenter_id': cls.workcenter2.id}),
|
||||
],
|
||||
})
|
||||
cls.bom_finished1.bom_line_ids[0].operation_id = cls.bom_finished1.operation_ids[0].id
|
||||
cls.bom_finished1.bom_line_ids[1].operation_id = cls.bom_finished1.operation_ids[1].id
|
||||
1776
odoo-bringout-oca-ocb-mrp/mrp/tests/test_bom.py
Normal file
1776
odoo-bringout-oca-ocb-mrp/mrp/tests/test_bom.py
Normal file
File diff suppressed because it is too large
Load diff
442
odoo-bringout-oca-ocb-mrp/mrp/tests/test_byproduct.py
Normal file
442
odoo-bringout-oca-ocb-mrp/mrp/tests/test_byproduct.py
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.tests import common
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class TestMrpByProduct(common.TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.MrpBom = cls.env['mrp.bom']
|
||||
cls.warehouse = cls.env.ref('stock.warehouse0')
|
||||
route_manufacture = cls.warehouse.manufacture_pull_id.route_id.id
|
||||
route_mto = cls.warehouse.mto_pull_id.route_id.id
|
||||
cls.uom_unit_id = cls.env.ref('uom.product_uom_unit').id
|
||||
def create_product(name, route_ids=[]):
|
||||
return cls.env['product.product'].create({
|
||||
'name': name,
|
||||
'type': 'product',
|
||||
'route_ids': route_ids})
|
||||
|
||||
# Create product A, B, C.
|
||||
# --------------------------
|
||||
cls.product_a = create_product('Product A', route_ids=[(6, 0, [route_manufacture, route_mto])])
|
||||
cls.product_b = create_product('Product B', route_ids=[(6, 0, [route_manufacture, route_mto])])
|
||||
cls.product_c_id = create_product('Product C', route_ids=[]).id
|
||||
cls.bom_byproduct = cls.MrpBom.create({
|
||||
'product_tmpl_id': cls.product_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'product_uom_id': cls.uom_unit_id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': cls.product_c_id, 'product_uom_id': cls.uom_unit_id, 'product_qty': 2})],
|
||||
'byproduct_ids': [(0, 0, {'product_id': cls.product_b.id, 'product_uom_id': cls.uom_unit_id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
def test_00_mrp_byproduct(self):
|
||||
""" Test by product with production order."""
|
||||
# Create BOM for product B
|
||||
# ------------------------
|
||||
bom_product_b = self.MrpBom.create({
|
||||
'product_tmpl_id': self.product_b.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'product_uom_id': self.uom_unit_id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})]
|
||||
})
|
||||
|
||||
# Create production order for product A
|
||||
# -------------------------------------
|
||||
|
||||
mnf_product_a_form = Form(self.env['mrp.production'])
|
||||
mnf_product_a_form.product_id = self.product_a
|
||||
mnf_product_a_form.bom_id = self.bom_byproduct
|
||||
mnf_product_a_form.product_qty = 2.0
|
||||
mnf_product_a = mnf_product_a_form.save()
|
||||
mnf_product_a.action_confirm()
|
||||
|
||||
# I confirm the production order.
|
||||
self.assertEqual(mnf_product_a.state, 'confirmed', 'Production order should be in state confirmed')
|
||||
|
||||
# Now I check the stock moves for the byproduct I created in the bill of material.
|
||||
# This move is created automatically when I confirmed the production order.
|
||||
moves = mnf_product_a.move_raw_ids | mnf_product_a.move_finished_ids
|
||||
self.assertTrue(moves, 'No moves are created !')
|
||||
|
||||
# I consume and produce the production of products.
|
||||
# I create record for selecting mode and quantity of products to produce.
|
||||
mo_form = Form(mnf_product_a)
|
||||
mo_form.qty_producing = 2.00
|
||||
mnf_product_a = mo_form.save()
|
||||
# I finish the production order.
|
||||
self.assertEqual(len(mnf_product_a.move_raw_ids), 1, "Wrong consume move on production order.")
|
||||
consume_move_c = mnf_product_a.move_raw_ids
|
||||
by_product_move = mnf_product_a.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_b.id)
|
||||
# Check sub production produced quantity...
|
||||
self.assertEqual(consume_move_c.product_uom_qty, 4, "Wrong consumed quantity of product c.")
|
||||
self.assertEqual(by_product_move.product_uom_qty, 2, "Wrong produced quantity of sub product.")
|
||||
|
||||
mnf_product_a._post_inventory()
|
||||
|
||||
# I see that stock moves of External Hard Disk including Headset USB are done now.
|
||||
self.assertFalse(any(move.state != 'done' for move in moves), 'Moves are not done!')
|
||||
|
||||
def test_01_mrp_byproduct(self):
|
||||
self.env["stock.quant"].create({
|
||||
"product_id": self.product_c_id,
|
||||
"location_id": self.warehouse.lot_stock_id.id,
|
||||
"quantity": 4,
|
||||
})
|
||||
bom_product_a = self.MrpBom.create({
|
||||
'product_tmpl_id': self.product_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'product_uom_id': self.uom_unit_id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})]
|
||||
})
|
||||
mnf_product_a_form = Form(self.env['mrp.production'])
|
||||
mnf_product_a_form.product_id = self.product_a
|
||||
mnf_product_a_form.bom_id = bom_product_a
|
||||
mnf_product_a_form.product_qty = 2.0
|
||||
mnf_product_a = mnf_product_a_form.save()
|
||||
mnf_product_a.action_confirm()
|
||||
self.assertEqual(mnf_product_a.state, "confirmed")
|
||||
mnf_product_a.move_raw_ids._action_assign()
|
||||
mnf_product_a.move_raw_ids.quantity_done = mnf_product_a.move_raw_ids.product_uom_qty
|
||||
mnf_product_a.move_raw_ids._action_done()
|
||||
self.assertEqual(mnf_product_a.state, "progress")
|
||||
mnf_product_a.qty_producing = 2
|
||||
mnf_product_a.button_mark_done()
|
||||
self.assertTrue(mnf_product_a.move_finished_ids)
|
||||
self.assertEqual(mnf_product_a.state, "done")
|
||||
|
||||
def test_change_product(self):
|
||||
""" Create a production order for a specific product with a BoM. Then change the BoM and the finished product for
|
||||
other ones and check the finished product of the first mo did not became a byproduct of the second one."""
|
||||
# Create BOM for product A with product B as component
|
||||
bom_product_a = self.MrpBom.create({
|
||||
'product_tmpl_id': self.product_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'product_uom_id': self.uom_unit_id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': self.product_b.id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})],
|
||||
})
|
||||
|
||||
bom_product_a_2 = self.MrpBom.create({
|
||||
'product_tmpl_id': self.product_b.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'product_uom_id': self.uom_unit_id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})],
|
||||
})
|
||||
# Create production order for product A
|
||||
# -------------------------------------
|
||||
|
||||
mnf_product_a_form = Form(self.env['mrp.production'])
|
||||
mnf_product_a_form.product_id = self.product_a
|
||||
mnf_product_a_form.bom_id = bom_product_a
|
||||
mnf_product_a_form.product_qty = 1.0
|
||||
mnf_product_a = mnf_product_a_form.save()
|
||||
mnf_product_a_form = Form(mnf_product_a)
|
||||
mnf_product_a_form.bom_id = bom_product_a_2
|
||||
mnf_product_a = mnf_product_a_form.save()
|
||||
self.assertEqual(mnf_product_a.move_raw_ids.product_id.id, self.product_c_id)
|
||||
self.assertFalse(mnf_product_a.move_byproduct_ids)
|
||||
|
||||
def test_default_uom(self):
|
||||
""" Tests the `uom_id` on the byproduct gets set automatically while creating a byproduct with a product,
|
||||
without the need to call an onchange or to set the uom manually in the create.
|
||||
"""
|
||||
# Set a specific UOM on the byproduct on purpose to make sure it's not just a default on the unit UOM
|
||||
# that makes the test pass.
|
||||
self.product_b.product_tmpl_id.uom_id = self.env.ref('uom.product_uom_dozen')
|
||||
bom = self.MrpBom.create({
|
||||
'product_tmpl_id': self.product_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'byproduct_ids': [(0, 0, {'product_id': self.product_b.id, 'product_qty': 1})]
|
||||
})
|
||||
self.assertEqual(bom.byproduct_ids.product_uom_id, self.env.ref('uom.product_uom_dozen'))
|
||||
|
||||
def test_finished_and_byproduct_moves(self):
|
||||
"""
|
||||
Tests the behavior of the `create` override in the model `mrp.production`
|
||||
regarding the values for the fields `move_finished_ids` and `move_byproduct_ids`.
|
||||
The behavior is a bit tricky, because the moves included in `move_byproduct_ids`
|
||||
are included in the `move_finished_ids`. `move_byproduct_ids` is a subset of `move_finished_ids`.
|
||||
|
||||
So, when creating a manufacturing order, whether:
|
||||
- Only `move_finished_ids` is passed, containing both the finished product and the by-products of the BOM,
|
||||
- Only `move_byproduct_ids` is passed, only containing the by-products of the BOM,
|
||||
- Both `move_finished_ids` and `move_byproduct_ids` are passed,
|
||||
holding the product finished and the byproducts respectively
|
||||
At the end, in the created manufacturing order
|
||||
`move_finished_ids` must contain both the finished product, and the by-products,
|
||||
`move_byproduct_ids` must contain only the by-products.
|
||||
|
||||
Besides, the code shouldn't raise an error
|
||||
because only one of the two `move_finished_ids`, `move_byproduct_ids` is provided.
|
||||
|
||||
In addition, the test voluntary sets a different produced quantity
|
||||
for the finished product and the by-products moves than defined in the BOM
|
||||
as it's the point to manually pass the `move_finished_ids` and `move_byproduct_ids`
|
||||
when creating a manufacturing order, set different values than the defaults, in this case
|
||||
a different produced quantity than the defaults from the BOM.
|
||||
"""
|
||||
bom_product_a = self.MrpBom.create({
|
||||
'product_tmpl_id': self.product_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2.0
|
||||
})],
|
||||
'byproduct_ids': [(0, 0, {
|
||||
'product_id': self.product_b.id, 'product_uom_id': self.uom_unit_id, 'product_qty': 1.0
|
||||
})]
|
||||
})
|
||||
for expected_finished_qty, expected_byproduct_qty, values in [
|
||||
# Only `move_finished_ids` passed, containing both the finished product and the by-product
|
||||
(3.0, 4.0, {
|
||||
'move_finished_ids': [
|
||||
(0, 0, {
|
||||
'product_id': self.product_a.id,
|
||||
'product_uom_qty': 3.0,
|
||||
'location_id': self.product_a.property_stock_production,
|
||||
'location_dest_id': self.warehouse.lot_stock_id.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'product_id': self.product_b.id,
|
||||
'product_uom_qty': 4.0,
|
||||
'location_id': self.product_a.property_stock_production,
|
||||
'location_dest_id': self.warehouse.lot_stock_id.id,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
# Only `move_byproduct_ids` passed, containing the by-product move only
|
||||
(2.0, 4.0, {
|
||||
'move_byproduct_ids': [
|
||||
(0, 0, {
|
||||
'product_id': self.product_b.id,
|
||||
'product_uom_qty': 4.0,
|
||||
'location_id': self.product_a.property_stock_production,
|
||||
'location_dest_id': self.warehouse.lot_stock_id.id,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
# Both `move_finished_ids` and `move_byproduct_ids` passed,
|
||||
# containing respectively the finished product and the by-product
|
||||
(3.0, 4.0, {
|
||||
'move_finished_ids': [
|
||||
(0, 0, {
|
||||
'product_id': self.product_a.id,
|
||||
'product_uom_qty': 3.0,
|
||||
'location_id': self.product_a.property_stock_production,
|
||||
'location_dest_id': self.warehouse.lot_stock_id.id,
|
||||
}),
|
||||
],
|
||||
'move_byproduct_ids': [
|
||||
(0, 0, {
|
||||
'product_id': self.product_b.id,
|
||||
'product_uom_qty': 4.0,
|
||||
'location_id': self.product_a.property_stock_production,
|
||||
'location_dest_id': self.warehouse.lot_stock_id.id,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]:
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': self.product_a.id,
|
||||
'bom_id': bom_product_a.id,
|
||||
'product_qty': 2.0,
|
||||
**values,
|
||||
})
|
||||
self.assertEqual(mo.move_finished_ids.product_id, self.product_a + self.product_b)
|
||||
self.assertEqual(mo.move_byproduct_ids.product_id, self.product_b)
|
||||
|
||||
finished_move = mo.move_finished_ids.filtered(lambda x: x.product_id == self.product_a)
|
||||
self.assertEqual(
|
||||
finished_move.product_uom_qty, expected_finished_qty, "Wrong produced quantity of finished product."
|
||||
)
|
||||
|
||||
by_product_move = mo.move_finished_ids.filtered(lambda x: x.product_id == self.product_b)
|
||||
self.assertEqual(
|
||||
by_product_move.product_uom_qty, expected_byproduct_qty, "Wrong produced quantity of by-product."
|
||||
)
|
||||
|
||||
# Also check the produced quantity of the by-product through `move_byproduct_ids`
|
||||
self.assertEqual(
|
||||
mo.move_byproduct_ids.product_uom_qty, expected_byproduct_qty, "Wrong produced quantity of by-product."
|
||||
)
|
||||
|
||||
def test_byproduct_putaway(self):
|
||||
"""
|
||||
Test the byproducts are dispatched correctly with putaway rules. We have
|
||||
a byproduct P and two sublocations L01, L02 with a capacity constraint:
|
||||
max 2 x P by location. There is already 1 x P at L01. Process a MO with
|
||||
2 x P as byproducts. They should be redirected to L02
|
||||
"""
|
||||
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
stor_category = self.env['stock.storage.category'].create({
|
||||
'name': 'Super Storage Category',
|
||||
'max_weight': 1000,
|
||||
'product_capacity_ids': [(0, 0, {
|
||||
'product_id': self.product_b.id,
|
||||
'quantity': 2,
|
||||
})]
|
||||
})
|
||||
shelf1_location = self.env['stock.location'].create({
|
||||
'name': 'shelf1',
|
||||
'usage': 'internal',
|
||||
'location_id': self.stock_location.id,
|
||||
'storage_category_id': stor_category.id,
|
||||
})
|
||||
shelf2_location = self.env['stock.location'].create({
|
||||
'name': 'shelf2',
|
||||
'usage': 'internal',
|
||||
'location_id': self.stock_location.id,
|
||||
'storage_category_id': stor_category.id,
|
||||
})
|
||||
self.env['stock.putaway.rule'].create({
|
||||
'product_id': self.product_b.id,
|
||||
'location_in_id': self.stock_location.id,
|
||||
'location_out_id': self.stock_location.id,
|
||||
'storage_category_id': stor_category.id,
|
||||
})
|
||||
self.env['stock.putaway.rule'].create({
|
||||
'product_id': self.product_a.id,
|
||||
'location_in_id': self.stock_location.id,
|
||||
'location_out_id': shelf2_location.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(self.product_b, shelf1_location, 1)
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_a
|
||||
mo_form.bom_id = self.bom_byproduct
|
||||
mo_form.product_qty = 2.0
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 2.00
|
||||
mo = mo_form.save()
|
||||
|
||||
mo._post_inventory()
|
||||
byproduct_move_line = mo.move_byproduct_ids.move_line_ids
|
||||
finished_move_line = mo.move_finished_ids.filtered(lambda m: m.product_id == self.product_a).move_line_ids
|
||||
self.assertEqual(byproduct_move_line.location_dest_id, shelf2_location)
|
||||
self.assertEqual(finished_move_line.location_dest_id, shelf2_location)
|
||||
|
||||
def test_check_byproducts_cost_share(self):
|
||||
"""
|
||||
Test that byproducts with total cost_share > 100% or a cost_share < 0%
|
||||
will throw a ValidationError
|
||||
"""
|
||||
# Create new MO
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_a
|
||||
mo_form.product_qty = 2.0
|
||||
mo = mo_form.save()
|
||||
|
||||
# Create product
|
||||
self.product_d = self.env['product.product'].create({
|
||||
'name': 'Product D',
|
||||
'type': 'product'})
|
||||
self.product_e = self.env['product.product'].create({
|
||||
'name': 'Product E',
|
||||
'type': 'product'})
|
||||
|
||||
# Create byproduct
|
||||
byproduct_1 = self.env['stock.move'].create({
|
||||
'name': 'By Product 1',
|
||||
'product_id': self.product_d.id,
|
||||
'product_uom': self.ref('uom.product_uom_unit'),
|
||||
'production_id': mo.id,
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_output'),
|
||||
'product_uom_qty': 0,
|
||||
'quantity_done': 0
|
||||
})
|
||||
byproduct_2 = self.env['stock.move'].create({
|
||||
'name': 'By Product 2',
|
||||
'product_id': self.product_e.id,
|
||||
'product_uom': self.ref('uom.product_uom_unit'),
|
||||
'production_id': mo.id,
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_output'),
|
||||
'product_uom_qty': 0,
|
||||
'quantity_done': 0
|
||||
})
|
||||
|
||||
# Update byproduct has cost share > 100%
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
byproduct_1.cost_share = 120
|
||||
mo.write({'move_byproduct_ids': [(4, byproduct_1.id)]})
|
||||
|
||||
# Update byproduct has cost share < 0%
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
byproduct_1.cost_share = -10
|
||||
mo.write({'move_byproduct_ids': [(4, byproduct_1.id)]})
|
||||
|
||||
# Update byproducts have total cost share > 100%
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
byproduct_1.cost_share = 60
|
||||
byproduct_2.cost_share = 70
|
||||
mo.write({'move_byproduct_ids': [(6, 0, [byproduct_1.id, byproduct_2.id])]})
|
||||
|
||||
def test_check_byproducts_cost_share_02(self):
|
||||
"""
|
||||
Test that byproducts with total cost_share < 100% with a cancelled moves will don't throw a ValidationError
|
||||
"""
|
||||
self.bom_byproduct.byproduct_ids[0].cost_share = 70
|
||||
self.bom_byproduct.byproduct_ids[0].product_qty = 2
|
||||
mo = self.env["mrp.production"].create({
|
||||
'product_id': self.product_a.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': self.bom_byproduct.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(mo.state, 'to_close')
|
||||
mo.move_byproduct_ids[0].quantity_done = 1
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
||||
def test_3_steps_byproduct(self):
|
||||
""" Test that non-bom byproducts are correctly pushed from
|
||||
post-production to the stock location in 3-steps manufacture. """
|
||||
self.warehouse.manufacture_steps = 'pbm_sam'
|
||||
self.env.user.groups_id += self.env.ref('mrp.group_mrp_byproducts')
|
||||
component, final_product, byproduct = self.env['product.product'].create([{
|
||||
'name': name,
|
||||
'type': 'product'
|
||||
} for name in ['Old Blood', 'Insight', 'Eyes on the Inside']])
|
||||
self.env['stock.quant']._update_available_quantity(component, self.warehouse.lot_stock_id, 1)
|
||||
mo = self.env["mrp.production"].create({
|
||||
'product_id': final_product.id,
|
||||
'product_qty': 1.0,
|
||||
})
|
||||
mo_form = Form(mo)
|
||||
with mo_form.move_raw_ids.new() as line:
|
||||
line.product_id = component
|
||||
with mo_form.move_byproduct_ids.new() as line:
|
||||
line.product_id = byproduct
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
preprod_picking = mo.picking_ids.filtered(lambda p: p.state == 'assigned')
|
||||
preprod_picking.move_line_ids.qty_done = 1
|
||||
preprod_picking.button_validate()
|
||||
mo.move_raw_ids.quantity_done = 1
|
||||
mo.qty_producing = 1
|
||||
mo.button_mark_done()
|
||||
postprod_picking = mo.picking_ids.filtered(lambda p: p.state == 'assigned')
|
||||
|
||||
self.assertEqual(len(postprod_picking.move_ids), 2)
|
||||
self.assertEqual(postprod_picking.move_ids.product_id, final_product + byproduct)
|
||||
self.assertEqual(postprod_picking.location_dest_id, self.warehouse.lot_stock_id)
|
||||
118
odoo-bringout-oca-ocb-mrp/mrp/tests/test_cancel_mo.py
Normal file
118
odoo-bringout-oca-ocb-mrp/mrp/tests/test_cancel_mo.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.fields import Datetime as Dt
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
|
||||
|
||||
class TestMrpCancelMO(TestMrpCommon):
|
||||
|
||||
def test_cancel_mo_without_routing_1(self):
|
||||
""" Cancel a Manufacturing Order with no routing, no production.
|
||||
"""
|
||||
# Create MO
|
||||
manufacturing_order = self.generate_mo()[0]
|
||||
# Do nothing, cancel it
|
||||
manufacturing_order.action_cancel()
|
||||
# Check the MO and its moves are cancelled
|
||||
self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'cancel',
|
||||
"Cancelled MO raw moves must be cancelled as well.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'cancel',
|
||||
"Cancelled MO raw moves must be cancelled as well.")
|
||||
self.assertEqual(manufacturing_order.move_finished_ids.state, 'cancel',
|
||||
"Cancelled MO finished move must be cancelled as well.")
|
||||
|
||||
def test_cancel_mo_without_routing_2(self):
|
||||
""" Cancel a Manufacturing Order with no routing but some productions.
|
||||
"""
|
||||
# Create MO
|
||||
manufacturing_order = self.generate_mo()[0]
|
||||
# Produce some quantity
|
||||
mo_form = Form(manufacturing_order)
|
||||
mo_form.qty_producing = 2
|
||||
manufacturing_order = mo_form.save()
|
||||
# Cancel it
|
||||
manufacturing_order.action_cancel()
|
||||
# Check it's cancelled
|
||||
self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'cancel',
|
||||
"Cancelled MO raw moves must be cancelled as well.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'cancel',
|
||||
"Cancelled MO raw moves must be cancelled as well.")
|
||||
self.assertEqual(manufacturing_order.move_finished_ids.state, 'cancel',
|
||||
"Cancelled MO finished move must be cancelled as well.")
|
||||
|
||||
def test_cancel_mo_without_routing_3(self):
|
||||
""" Cancel a Manufacturing Order with no routing but some productions
|
||||
after post inventory.
|
||||
"""
|
||||
# Create MO
|
||||
manufacturing_order = self.generate_mo(consumption='strict')[0]
|
||||
# Produce some quantity (not all to avoid to done the MO when post inventory)
|
||||
mo_form = Form(manufacturing_order)
|
||||
mo_form.qty_producing = 2
|
||||
manufacturing_order = mo_form.save()
|
||||
# Post Inventory
|
||||
manufacturing_order._post_inventory()
|
||||
# Cancel the MO
|
||||
manufacturing_order.action_cancel()
|
||||
# Check MO is marked as done and its SML are done or cancelled
|
||||
self.assertEqual(manufacturing_order.state, 'done', "MO should be in done state.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'done',
|
||||
"Due to 'post_inventory', some move raw must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'done',
|
||||
"Due to 'post_inventory', some move raw must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[2].state, 'cancel',
|
||||
"The other move raw are cancelled like their MO.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[3].state, 'cancel',
|
||||
"The other move raw are cancelled like their MO.")
|
||||
self.assertEqual(manufacturing_order.move_finished_ids[0].state, 'done',
|
||||
"Due to 'post_inventory', a move finished must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
|
||||
"The other move finished is cancelled like its MO.")
|
||||
|
||||
def test_unlink_mo(self):
|
||||
""" Try to unlink a Manufacturing Order, and check it's possible or not
|
||||
depending of the MO state (must be in cancel state to be unlinked, but
|
||||
the unlink method will try to cancel MO before unlink them).
|
||||
"""
|
||||
# Case #1: Create MO, do nothing and try to unlink it (can be deleted)
|
||||
manufacturing_order = self.generate_mo()[0]
|
||||
self.assertEqual(manufacturing_order.exists().state, 'confirmed')
|
||||
manufacturing_order.unlink()
|
||||
# Check the MO is deleted.
|
||||
self.assertEqual(manufacturing_order.exists().state, False)
|
||||
|
||||
# Case #2: Create MO, make and post some production, then try to unlink
|
||||
# it (cannot be deleted)
|
||||
manufacturing_order = self.generate_mo()[0]
|
||||
# Produce some quantity (not all to avoid to done the MO when post inventory)
|
||||
mo_form = Form(manufacturing_order)
|
||||
mo_form.qty_producing = 2
|
||||
manufacturing_order = mo_form.save()
|
||||
# Post Inventory
|
||||
manufacturing_order._post_inventory()
|
||||
# Unlink the MO must raises an UserError since it cannot be really cancelled
|
||||
self.assertEqual(manufacturing_order.exists().state, 'progress')
|
||||
with self.assertRaises(UserError):
|
||||
manufacturing_order.unlink()
|
||||
|
||||
def test_cancel_mo_without_component(self):
|
||||
product_form = Form(self.env['product.product'])
|
||||
product_form.name = "SuperProduct"
|
||||
product = product_form.save()
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product
|
||||
mo = mo_form.save()
|
||||
|
||||
mo.action_confirm()
|
||||
mo.action_cancel()
|
||||
|
||||
self.assertEqual(mo.move_finished_ids.state, 'cancel')
|
||||
self.assertEqual(mo.state, 'cancel')
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
from odoo.tests import tagged
|
||||
from odoo.addons.mrp.tests.common_consume_tracked_component import TestConsumeTrackedComponentCommon
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestConsumeTrackedComponent(TestConsumeTrackedComponentCommon):
|
||||
|
||||
def test_option_disabled_and_qty_available(self):
|
||||
"""
|
||||
Option disabled, qty available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are only consumed on button_mark_done trigger
|
||||
"""
|
||||
|
||||
self.picking_type.use_auto_consume_components_lots = False
|
||||
|
||||
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
|
||||
mo_all = mo_none + mo_serial + mo_lot
|
||||
mo_all.action_confirm()
|
||||
|
||||
all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT
|
||||
|
||||
quant = self.create_quant(self.raw_none, 3*all_qty)
|
||||
quant |= self.create_quant(self.raw_lot, 2*all_qty)
|
||||
quant |= self.create_quant(self.raw_serial, 1*all_qty)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
#Quantities are fully reserved (stock.move state is available)
|
||||
mo_all.action_assign()
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.")
|
||||
|
||||
#Test for Serial Product
|
||||
self.executeConsumptionTriggers(mo_serial)
|
||||
self.executeConsumptionTriggers(mo_none)
|
||||
self.executeConsumptionTriggers(mo_lot)
|
||||
for mov in mo_all.move_raw_ids:
|
||||
if mov.has_tracking == 'none' or mov.raw_material_production_id.state == 'done':
|
||||
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
else:
|
||||
self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to 0.")
|
||||
|
||||
def test_option_enabled_and_qty_available(self):
|
||||
"""
|
||||
Option enabled, qty available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are fully consumed
|
||||
"""
|
||||
|
||||
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
|
||||
mo_all = mo_none + mo_serial + mo_lot
|
||||
mo_all.action_confirm()
|
||||
|
||||
all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT
|
||||
|
||||
quant = self.create_quant(self.raw_none, 3*all_qty)
|
||||
quant |= self.create_quant(self.raw_lot, 2*all_qty)
|
||||
quant |= self.create_quant(self.raw_serial, 1*all_qty)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
#Quantities are fully reserved (stock.move state is available)
|
||||
mo_all.action_assign()
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.")
|
||||
|
||||
self.executeConsumptionTriggers(mo_serial)
|
||||
self.executeConsumptionTriggers(mo_none)
|
||||
self.executeConsumptionTriggers(mo_lot)
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
|
||||
def test_option_enabled_and_qty_not_available(self):
|
||||
"""
|
||||
Option enabled, qty not available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are not consumed
|
||||
"""
|
||||
|
||||
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_TRIGGERS_COUNT)
|
||||
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_TRIGGERS_COUNT)
|
||||
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_TRIGGERS_COUNT)
|
||||
|
||||
mo_all = mo_none + mo_serial + mo_lot
|
||||
mo_all.action_confirm()
|
||||
|
||||
#Quantities are not reserved at all (stock.move state is confirmed)
|
||||
mo_all.action_assign()
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(0, mov.reserved_availability, "Reserved quantity shall be equal to 0.")
|
||||
|
||||
self.executeConsumptionTriggers(mo_serial)
|
||||
self.executeConsumptionTriggers(mo_none)
|
||||
self.executeConsumptionTriggers(mo_lot)
|
||||
|
||||
for mov in mo_all.move_raw_ids:
|
||||
if mov.has_tracking == 'none':
|
||||
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
else:
|
||||
self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
|
||||
def test_option_enabled_and_qty_partially_available(self):
|
||||
"""
|
||||
Option enabled, qty partially available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are partially consumed
|
||||
"""
|
||||
|
||||
#update BoM serial component qty
|
||||
self.bom_none_lines[2].product_qty = 2
|
||||
self.bom_serial_lines[2].product_qty = 2
|
||||
self.bom_lot_lines[2].product_qty = 2
|
||||
|
||||
raw_none_qty = 2
|
||||
raw_tracked_qty = 1
|
||||
|
||||
quant = self.create_quant(self.raw_none, raw_none_qty)
|
||||
quant |= self.create_quant(self.raw_lot, raw_tracked_qty)
|
||||
quant |= self.create_quant(self.raw_serial, raw_tracked_qty)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
#We must create & process each MO at once as we must assign quants for each individually
|
||||
def testUnit(mo_tmpl, serialTrigger=None):
|
||||
mo = self.create_mo(mo_tmpl, 1)
|
||||
mo.action_confirm()
|
||||
|
||||
#Quantities are partially reserved (stock.move state is partially_available)
|
||||
mo.action_assign()
|
||||
for mov in mo.move_raw_ids:
|
||||
if mov.has_tracking == "none":
|
||||
self.assertEqual(raw_none_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_none_qty)+ ".")
|
||||
else:
|
||||
self.assertEqual(raw_tracked_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_tracked_qty)+ ".")
|
||||
|
||||
if serialTrigger is None:
|
||||
self.executeConsumptionTriggers(mo)
|
||||
elif serialTrigger == 1:
|
||||
mo.qty_producing = mo.product_qty
|
||||
mo._onchange_producing()
|
||||
elif serialTrigger == 2:
|
||||
mo.action_generate_serial()
|
||||
|
||||
for mov in mo.move_raw_ids:
|
||||
if mov.has_tracking == "none":
|
||||
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
else:
|
||||
self.assertEqual(raw_tracked_qty, mov.quantity_done, "Done quantity shall be equal to " + str(raw_tracked_qty)+ ".")
|
||||
mo.action_cancel()
|
||||
|
||||
testUnit(self.mo_none_tmpl)
|
||||
testUnit(self.mo_lot_tmpl)
|
||||
testUnit(self.mo_serial_tmpl, 1)
|
||||
testUnit(self.mo_serial_tmpl, 2)
|
||||
190
odoo-bringout-oca-ocb-mrp/mrp/tests/test_manual_consumption.py
Normal file
190
odoo-bringout-oca-ocb-mrp/mrp/tests/test_manual_consumption.py
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import tagged, Form, HttpCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTourManualConsumption(HttpCase):
|
||||
def test_mrp_manual_consumption(self):
|
||||
"""Test manual consumption mechanism. Test when manual consumption is
|
||||
True, quantity_done won't be updated automatically. Bom line with tracked
|
||||
products or operations should be set to manual consumption automatically.
|
||||
Also test that when manually change quantity_done, manual consumption
|
||||
will be set to True. Also test when create backorder, the manual consumption
|
||||
should be set according to the bom.
|
||||
"""
|
||||
Product = self.env['product.product']
|
||||
product_finish = Product.create({
|
||||
'name': 'finish',
|
||||
'type': 'product',
|
||||
'tracking': 'none',})
|
||||
product_nt = Product.create({
|
||||
'name': 'No tracking',
|
||||
'type': 'product',
|
||||
'tracking': 'none',})
|
||||
product_sn = Product.create({
|
||||
'name': 'Serial',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',})
|
||||
product_lot = Product.create({
|
||||
'name': 'Lot',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': product_finish.id,
|
||||
'product_tmpl_id': product_finish.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_nt.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': product_sn.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': product_lot.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_finish
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 10
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
|
||||
# test no updating
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5
|
||||
mo = mo_form.save()
|
||||
move_nt, move_sn, move_lot = mo.move_raw_ids
|
||||
self.assertEqual(move_nt.manual_consumption, False)
|
||||
self.assertEqual(move_nt.quantity_done, 5)
|
||||
self.assertEqual(move_sn.manual_consumption, True)
|
||||
self.assertEqual(move_sn.quantity_done, 0)
|
||||
self.assertEqual(move_lot.manual_consumption, True)
|
||||
self.assertEqual(move_lot.quantity_done, 0)
|
||||
|
||||
action_id = self.env.ref('mrp.menu_mrp_production_action').action
|
||||
url = "/web#model=mrp.production&view_type=form&action=%s&id=%s" % (str(action_id.id), str(mo.id))
|
||||
self.start_tour(url, "test_mrp_manual_consumption", login="admin", timeout=200)
|
||||
|
||||
self.assertEqual(move_nt.manual_consumption, True)
|
||||
self.assertEqual(move_nt.quantity_done, 6.0)
|
||||
self.assertEqual(move_sn.manual_consumption, True)
|
||||
self.assertEqual(move_sn.quantity_done, 0)
|
||||
self.assertEqual(move_lot.manual_consumption, True)
|
||||
self.assertEqual(move_lot.quantity_done, 0)
|
||||
|
||||
backorder = mo.procurement_group_id.mrp_production_ids - mo
|
||||
move_nt = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_nt)
|
||||
move_sn = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_sn)
|
||||
move_lot = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_lot)
|
||||
self.assertEqual(move_nt.manual_consumption, False)
|
||||
self.assertEqual(move_sn.manual_consumption, True)
|
||||
self.assertEqual(move_lot.manual_consumption, True)
|
||||
|
||||
|
||||
class TestManualConsumption(TestMrpCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
|
||||
|
||||
def test_manual_consumption_backorder_00(self):
|
||||
"""Test when use_auto_consume_components_lots is not set, manual consumption
|
||||
of the backorder is correctly set.
|
||||
"""
|
||||
mo, _, _final, c1, c2 = self.generate_mo('none', 'lot', 'none', qty_final=2)
|
||||
|
||||
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot',
|
||||
'product_id': c1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(c1, self.stock_location, 8, lot_id=lot)
|
||||
self.env['stock.quant']._update_available_quantity(c2, self.stock_location, 2)
|
||||
|
||||
mo.action_assign()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.save()
|
||||
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c1.id).mapped("quantity_done")), 0)
|
||||
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c2.id).mapped("quantity_done")), 1)
|
||||
|
||||
details_operation_form = Form(mo.move_raw_ids.filtered(lambda m: m.product_id == c1), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 4
|
||||
ml.lot_id = lot
|
||||
details_operation_form.save()
|
||||
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
backorder_mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
self.assertTrue(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
|
||||
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
|
||||
|
||||
def test_manual_consumption_backorder_01(self):
|
||||
"""Test when use_auto_consume_components_lots is set, manual consumption
|
||||
of the backorder is correctly set.
|
||||
"""
|
||||
picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
|
||||
picking_type.use_auto_consume_components_lots = True
|
||||
|
||||
mo, _, _final, c1, c2 = self.generate_mo('none', 'lot', 'none', qty_final=2)
|
||||
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot',
|
||||
'product_id': c1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(c1, self.stock_location, 8, lot_id=lot)
|
||||
self.env['stock.quant']._update_available_quantity(c2, self.stock_location, 2)
|
||||
|
||||
mo.action_assign()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.save()
|
||||
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c1.id).mapped("quantity_done")), 4)
|
||||
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c2.id).mapped("quantity_done")), 1)
|
||||
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
backorder_mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
|
||||
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
|
||||
|
||||
def test_manual_consumption_split_merge_00(self):
|
||||
"""Test manual consumption is correctly set after split or merge.
|
||||
"""
|
||||
# Change 'Units' rounding to 1 (integer only quantities)
|
||||
self.uom_unit.rounding = 1
|
||||
# Create a mo for 10 products
|
||||
mo, _, _, p1, p2 = self.generate_mo('none', 'lot', 'none', qty_final=10)
|
||||
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)
|
||||
|
||||
# Split in 3 parts
|
||||
action = mo.action_split()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
wizard.counter = 3
|
||||
action = wizard.save().action_split()
|
||||
for production in mo.procurement_group_id.mrp_production_ids:
|
||||
self.assertTrue(production.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
|
||||
self.assertFalse(production.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)
|
||||
|
||||
# Merge them back
|
||||
action = mo.procurement_group_id.mrp_production_ids.action_merge()
|
||||
mo = self.env[action['res_model']].browse(action['res_id'])
|
||||
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)
|
||||
284
odoo-bringout-oca-ocb-mrp/mrp/tests/test_multicompany.py
Normal file
284
odoo-bringout-oca-ocb-mrp/mrp/tests/test_multicompany.py
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import common, Form
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestMrpMulticompany(common.TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
|
||||
|
||||
group_user = cls.env.ref('base.group_user')
|
||||
group_mrp_manager = cls.env.ref('mrp.group_mrp_manager')
|
||||
cls.company_a = cls.env['res.company'].create({'name': 'Company A'})
|
||||
cls.company_b = cls.env['res.company'].create({'name': 'Company B'})
|
||||
cls.warehouse_a = cls.env['stock.warehouse'].search([('company_id', '=', cls.company_a.id)], limit=1)
|
||||
cls.warehouse_b = cls.env['stock.warehouse'].search([('company_id', '=', cls.company_b.id)], limit=1)
|
||||
cls.stock_location_a = cls.warehouse_a.lot_stock_id
|
||||
cls.stock_location_b = cls.warehouse_b.lot_stock_id
|
||||
|
||||
cls.user_a = cls.env['res.users'].create({
|
||||
'name': 'user company a with access to company b',
|
||||
'login': 'user a',
|
||||
'groups_id': [(6, 0, [group_user.id, group_mrp_manager.id])],
|
||||
'company_id': cls.company_a.id,
|
||||
'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
|
||||
})
|
||||
cls.user_b = cls.env['res.users'].create({
|
||||
'name': 'user company a with access to company b',
|
||||
'login': 'user b',
|
||||
'groups_id': [(6, 0, [group_user.id, group_mrp_manager.id])],
|
||||
'company_id': cls.company_b.id,
|
||||
'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
|
||||
})
|
||||
|
||||
def test_bom_1(self):
|
||||
"""Check it is not possible to use a product of Company B in a
|
||||
bom of Company A. """
|
||||
|
||||
product_b = self.env['product.product'].create({
|
||||
'name': 'p1',
|
||||
'company_id': self.company_b.id,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_b.id,
|
||||
'product_tmpl_id': product_b.product_tmpl_id.id,
|
||||
'company_id': self.company_a.id,
|
||||
})
|
||||
|
||||
def test_bom_2(self):
|
||||
"""Check it is not possible to use a product of Company B as a component
|
||||
in a bom of Company A. """
|
||||
|
||||
product_a = self.env['product.product'].create({
|
||||
'name': 'p1',
|
||||
'company_id': self.company_a.id,
|
||||
})
|
||||
product_b = self.env['product.product'].create({
|
||||
'name': 'p2',
|
||||
'company_id': self.company_b.id,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_a.id,
|
||||
'product_tmpl_id': product_b.product_tmpl_id.id,
|
||||
'company_id': self.company_a.id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': product_b.id})]
|
||||
})
|
||||
|
||||
def test_production_1(self):
|
||||
"""Check it is not possible to confirm a production of Company B with
|
||||
product of Company A. """
|
||||
|
||||
product_a = self.env['product.product'].create({
|
||||
'name': 'p1',
|
||||
'company_id': self.company_a.id,
|
||||
})
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': product_a.id,
|
||||
'product_uom_id': product_a.uom_id.id,
|
||||
'company_id': self.company_b.id,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
mo.action_confirm()
|
||||
|
||||
def test_production_2(self):
|
||||
"""Check that confirming a production in company b with user_a will create
|
||||
stock moves on company b. """
|
||||
|
||||
product_a = self.env['product.product'].create({
|
||||
'name': 'p1',
|
||||
'company_id': self.company_a.id,
|
||||
})
|
||||
component_a = self.env['product.product'].create({
|
||||
'name': 'p2',
|
||||
'company_id': self.company_a.id,
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_a.id,
|
||||
'product_tmpl_id': product_a.product_tmpl_id.id,
|
||||
'company_id': self.company_a.id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': component_a.id})]
|
||||
})
|
||||
mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
|
||||
mo_form.product_id = product_a
|
||||
mo = mo_form.save()
|
||||
mo.with_user(self.user_b).action_confirm()
|
||||
self.assertEqual(mo.move_raw_ids.company_id, self.company_a)
|
||||
self.assertEqual(mo.move_finished_ids.company_id, self.company_a)
|
||||
|
||||
def test_product_produce_1(self):
|
||||
"""Check that using a finished lot of company b in the produce wizard of a production
|
||||
of company a is not allowed """
|
||||
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'p1',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'p2',
|
||||
})
|
||||
lot_b = self.env['stock.lot'].create({
|
||||
'product_id': product.id,
|
||||
'company_id': self.company_b.id,
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product.id,
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'company_id': self.company_a.id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': component.id})]
|
||||
})
|
||||
mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
|
||||
mo_form.product_id = product
|
||||
# The mo must be confirmed, no longer in draft, in order for `lot_producing_id` to be visible in the view
|
||||
# <div class="o_row" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}">
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.lot_producing_id = lot_b
|
||||
mo = mo_form.save()
|
||||
with self.assertRaises(UserError):
|
||||
mo.with_user(self.user_b).action_confirm()
|
||||
|
||||
def test_product_produce_2(self):
|
||||
"""Check that using a component lot of company b in the produce wizard of a production
|
||||
of company a is not allowed """
|
||||
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'p1',
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'p2',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
lot_b = self.env['stock.lot'].create({
|
||||
'product_id': component.id,
|
||||
'company_id': self.company_b.id,
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product.id,
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'company_id': self.company_a.id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': component.id})]
|
||||
})
|
||||
mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
|
||||
mo_form.product_id = product
|
||||
mo = mo_form.save()
|
||||
mo.with_user(self.user_b).action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = lot_b
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
with self.assertRaises(UserError):
|
||||
mo.button_mark_done()
|
||||
|
||||
def test_is_kit_in_multi_company_env(self):
|
||||
""" Check that is_kits is company dependant """
|
||||
product1, product2 = self.env['product.product'].create([{'name': 'Kit Kat'}, {'name': 'twix'}])
|
||||
self.env['mrp.bom'].create([{
|
||||
'product_id': product1.id,
|
||||
'product_tmpl_id': product1.product_tmpl_id.id,
|
||||
'company_id': self.company_a.id,
|
||||
'type': 'phantom',
|
||||
}, {
|
||||
'product_id': product2.id,
|
||||
'product_tmpl_id': product2.product_tmpl_id.id,
|
||||
'company_id': False,
|
||||
'type': 'phantom',
|
||||
}])
|
||||
template1 = product1.product_tmpl_id
|
||||
template2 = product2.product_tmpl_id
|
||||
|
||||
self.assertFalse(product1.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
|
||||
self.assertFalse(template1.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
|
||||
self.assertTrue(product1.with_company(self.company_a).is_kits)
|
||||
self.assertTrue(template1.with_company(self.company_a).is_kits)
|
||||
self.assertFalse(product1.with_company(self.company_b).is_kits)
|
||||
self.assertFalse(template1.with_company(self.company_b).is_kits)
|
||||
|
||||
self.assertTrue(product2.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
|
||||
self.assertTrue(template2.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
|
||||
self.assertTrue(product2.with_company(self.company_a).is_kits)
|
||||
self.assertTrue(template2.with_company(self.company_a).is_kits)
|
||||
self.assertTrue(product2.with_company(self.company_b).is_kits)
|
||||
self.assertTrue(template2.with_company(self.company_b).is_kits)
|
||||
|
||||
def test_partner_1(self):
|
||||
""" On a product without company, as a user of Company B, check it is not possible to use a
|
||||
location limited to Company A as `property_stock_production` """
|
||||
|
||||
shared_product = self.env['product.product'].create({
|
||||
'name': 'Shared Product',
|
||||
'company_id': False,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
shared_product.with_user(self.user_b).property_stock_production = self.stock_location_a
|
||||
|
||||
def test_company_specific_routes_and_company_creation(self):
|
||||
"""
|
||||
Setup: company-specific manufacture routes
|
||||
Use case: create a new company
|
||||
A manufacture route should be created for the new company
|
||||
"""
|
||||
company = self.env.company
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1)
|
||||
|
||||
manufacture_rule = warehouse.manufacture_pull_id
|
||||
manufacture_route = manufacture_rule.route_id
|
||||
|
||||
# Allocate each company-specific manufacture rule to a new route
|
||||
for rule in manufacture_route.rule_ids.sudo():
|
||||
rule_company = rule.company_id
|
||||
if not rule_company or rule_company == company:
|
||||
continue
|
||||
manufacture_route.copy({
|
||||
'company_id': rule_company.id,
|
||||
'rule_ids': [(4, rule.id)],
|
||||
})
|
||||
# Also specify the company of the "generic route" (the one from the master data)
|
||||
manufacture_route.company_id = company
|
||||
|
||||
new_company = self.env['res.company'].create({'name': 'Super Company'})
|
||||
new_warehouse = self.env['stock.warehouse'].search([('company_id', '=', new_company.id)], limit=1)
|
||||
self.assertEqual(new_warehouse.manufacture_pull_id.route_id.company_id, new_company)
|
||||
|
||||
def test_company_specific_routes_and_warehouse_creation(self):
|
||||
""" Check that we are able to create a new warehouse when the generic manufacture route
|
||||
is in a different company. """
|
||||
group_stock_manager = self.env.ref('stock.group_stock_manager')
|
||||
self.user_a.write({'groups_id': [(4, group_stock_manager.id)]})
|
||||
|
||||
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture')
|
||||
for rule in manufacture_route.rule_ids.sudo():
|
||||
rule_company = rule.company_id
|
||||
if not rule_company or rule_company == self.company_a:
|
||||
continue
|
||||
manufacture_route.copy({
|
||||
'company_id': rule_company.id,
|
||||
'rule_ids': [(4, rule.id)],
|
||||
})
|
||||
manufacture_route.company_id = self.company_a
|
||||
|
||||
# Enable multi warehouse
|
||||
group_user = self.env.ref('base.group_user')
|
||||
group_stock_multi_warehouses = self.env.ref('stock.group_stock_multi_warehouses')
|
||||
group_stock_multi_locations = self.env.ref('stock.group_stock_multi_locations')
|
||||
self.env['res.config.settings'].create({
|
||||
'group_stock_multi_locations': True,
|
||||
}).execute()
|
||||
group_user.write({'implied_ids': [(4, group_stock_multi_warehouses.id), (4, group_stock_multi_locations.id)]})
|
||||
|
||||
new_warehouse = self.env['stock.warehouse'].with_user(self.user_a).with_context(allowed_company_ids=[self.company_b.id]).create({
|
||||
'name': 'Warehouse #2',
|
||||
'code': 'WH2',
|
||||
})
|
||||
self.assertEqual(new_warehouse.manufacture_pull_id.route_id.company_id, self.company_b)
|
||||
71
odoo-bringout-oca-ocb-mrp/mrp/tests/test_oee.py
Normal file
71
odoo-bringout-oca-ocb-mrp/mrp/tests/test_oee.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta, time
|
||||
from pytz import timezone, utc
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
|
||||
|
||||
class TestOee(TestMrpCommon):
|
||||
def create_productivity_line(self, loss_reason, date_start=False, date_end=False):
|
||||
return self.env['mrp.workcenter.productivity'].create({
|
||||
'workcenter_id': self.workcenter_1.id,
|
||||
'date_start': date_start,
|
||||
'date_end': date_end,
|
||||
'loss_id': loss_reason.id,
|
||||
'description': loss_reason.name
|
||||
})
|
||||
|
||||
def test_wrokcenter_oee(self):
|
||||
""" Test case workcenter oee. """
|
||||
day = datetime.date(datetime.today())
|
||||
# Make the test work the weekend. It will fails due to workcenter working hours.
|
||||
if day.weekday() in (5, 6):
|
||||
day -= timedelta(days=2)
|
||||
|
||||
tz = timezone(self.workcenter_1.resource_calendar_id.tz)
|
||||
|
||||
def time_to_string_utc_datetime(time):
|
||||
return fields.Datetime.to_string(
|
||||
tz.localize(datetime.combine(day, time)).astimezone(utc)
|
||||
)
|
||||
|
||||
start_time = time_to_string_utc_datetime(time(10, 43, 22))
|
||||
end_time = time_to_string_utc_datetime(time(10, 56, 22))
|
||||
# Productive time duration (13 min)
|
||||
self.create_productivity_line(self.env.ref('mrp.block_reason7'), start_time, end_time)
|
||||
|
||||
# Material Availability time duration (1.52 min)
|
||||
# Check working state is blocked or not.
|
||||
start_time = time_to_string_utc_datetime(time(10, 47, 8))
|
||||
workcenter_productivity_1 = self.create_productivity_line(self.env.ref('mrp.block_reason0'), start_time)
|
||||
self.assertEqual(self.workcenter_1.working_state, 'blocked', "Wrong working state of workcenter.")
|
||||
|
||||
# Check working state is normal or not.
|
||||
end_time = time_to_string_utc_datetime(time(10, 48, 39))
|
||||
workcenter_productivity_1.write({'date_end': end_time})
|
||||
self.assertEqual(self.workcenter_1.working_state, 'normal', "Wrong working state of workcenter.")
|
||||
|
||||
# Process Defect time duration (1.33 min)
|
||||
start_time = time_to_string_utc_datetime(time(10, 48, 38))
|
||||
end_time = time_to_string_utc_datetime(time(10, 49, 58))
|
||||
self.create_productivity_line(self.env.ref('mrp.block_reason5'), start_time, end_time)
|
||||
# Reduced Speed time duration (3.0 min)
|
||||
start_time = time_to_string_utc_datetime(time(10, 50, 22))
|
||||
end_time = time_to_string_utc_datetime(time(10, 53, 22))
|
||||
self.create_productivity_line(self.env.ref('mrp.block_reason4'), start_time, end_time)
|
||||
|
||||
# Block time : ( Process Defact (1.33 min) + Reduced Speed (3.0 min) + Material Availability (1.52 min)) = 5.85 min
|
||||
blocked_time_in_hour = round(((1.33 + 3.0 + 1.52) / 60.0), 2)
|
||||
# Productive time : Productive time duration (13 min)
|
||||
productive_time_in_hour = round((13.0 / 60.0), 2)
|
||||
|
||||
# Check blocked time and productive time
|
||||
self.assertEqual(self.workcenter_1.blocked_time, blocked_time_in_hour, "Wrong block time on workcenter.")
|
||||
self.assertEqual(self.workcenter_1.productive_time, productive_time_in_hour, "Wrong productive time on workcenter.")
|
||||
|
||||
# Check overall equipment effectiveness
|
||||
computed_oee = round(((productive_time_in_hour * 100.0)/(productive_time_in_hour + blocked_time_in_hour)), 2)
|
||||
self.assertEqual(self.workcenter_1.oee, computed_oee, "Wrong oee on workcenter.")
|
||||
4170
odoo-bringout-oca-ocb-mrp/mrp/tests/test_order.py
Normal file
4170
odoo-bringout-oca-ocb-mrp/mrp/tests/test_order.py
Normal file
File diff suppressed because it is too large
Load diff
112
odoo-bringout-oca-ocb-mrp/mrp/tests/test_performance.py
Normal file
112
odoo-bringout-oca-ocb-mrp/mrp/tests/test_performance.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import unittest
|
||||
import time
|
||||
import logging
|
||||
|
||||
from odoo.tests import common, Form
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestMrpSerialMassProducePerformance(common.TransactionCase):
|
||||
|
||||
@unittest.skip
|
||||
def test_smp_performance(self):
|
||||
|
||||
total_quantity = 1000
|
||||
quantity = 1
|
||||
|
||||
raw_materials_count = 10
|
||||
trackings = [
|
||||
'none',
|
||||
# 'lot',
|
||||
# 'serial'
|
||||
]
|
||||
|
||||
_logger.info('setting up environment')
|
||||
|
||||
raw_materials = []
|
||||
for i in range(raw_materials_count):
|
||||
raw_materials.append(self.env['product.product'].create({
|
||||
'name': '@raw_material#' + str(i + 1),
|
||||
'type': 'product',
|
||||
'tracking': trackings[i % len(trackings)]
|
||||
}))
|
||||
finished = self.env['product.product'].create({
|
||||
'name': '@finished',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': finished.id,
|
||||
'product_tmpl_id': finished.product_tmpl_id.id,
|
||||
'product_uom_id': finished.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {'product_id': p[0]['id'], 'product_qty': 1}) for p in raw_materials]
|
||||
})
|
||||
|
||||
form = Form(self.env['mrp.production'])
|
||||
form.product_id = finished
|
||||
form.bom_id = bom
|
||||
form.product_qty = total_quantity
|
||||
|
||||
mo = form.save()
|
||||
|
||||
mo.action_confirm()
|
||||
|
||||
for i in range(raw_materials_count):
|
||||
if raw_materials[i].tracking == 'none':
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': raw_materials[i].id,
|
||||
'inventory_quantity': total_quantity,
|
||||
'location_id': mo.location_src_id.id,
|
||||
})._apply_inventory()
|
||||
elif raw_materials[i].tracking == 'lot':
|
||||
qty = total_quantity
|
||||
while qty > 0:
|
||||
lot = self.env['stock.lot'].create({
|
||||
'product_id': raw_materials[i].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': raw_materials[i].id,
|
||||
'inventory_quantity': 10,
|
||||
'location_id': mo.location_src_id.id,
|
||||
'lot_id': lot.id,
|
||||
})._apply_inventory()
|
||||
qty -= 10
|
||||
else:
|
||||
for _ in range(total_quantity):
|
||||
lot = self.env['stock.lot'].create({
|
||||
'product_id': raw_materials[i].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': raw_materials[i].id,
|
||||
'inventory_quantity': 1,
|
||||
'location_id': mo.location_src_id.id,
|
||||
'lot_id': lot.id,
|
||||
})._apply_inventory()
|
||||
|
||||
mo.action_assign()
|
||||
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = quantity
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard = wizard.save()
|
||||
|
||||
_logger.info('generating serial numbers')
|
||||
start = time.perf_counter()
|
||||
if quantity == total_quantity:
|
||||
wizard.apply()
|
||||
else:
|
||||
wizard.create_backorder()
|
||||
end = time.perf_counter()
|
||||
_logger.info('time to produce %s/%s: %s', quantity, total_quantity, end - start)
|
||||
788
odoo-bringout-oca-ocb-mrp/mrp/tests/test_procurement.py
Normal file
788
odoo-bringout-oca-ocb-mrp/mrp/tests/test_procurement.py
Normal file
|
|
@ -0,0 +1,788 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestProcurement(TestMrpCommon):
|
||||
|
||||
def test_procurement(self):
|
||||
"""This test case when create production order check procurement is create"""
|
||||
# Update BOM
|
||||
self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
|
||||
self.bom_1.bom_line_ids.filtered(lambda x: x.product_id == self.product_1).unlink()
|
||||
# Update route
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
self.warehouse.mto_pull_id.route_id.active = True
|
||||
route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
|
||||
route_mto = self.warehouse.mto_pull_id.route_id.id
|
||||
self.product_4.write({'route_ids': [(6, 0, [route_manufacture, route_mto])]})
|
||||
|
||||
# Create production order
|
||||
# -------------------------
|
||||
# Product6 Unit 24
|
||||
# Product4 8 Dozen
|
||||
# Product2 12 Unit
|
||||
# -----------------------
|
||||
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.product_6
|
||||
production_form.bom_id = self.bom_3
|
||||
production_form.product_qty = 24
|
||||
production_form.product_uom_id = self.product_6.uom_id
|
||||
production_product_6 = production_form.save()
|
||||
production_product_6.action_confirm()
|
||||
production_product_6.action_assign()
|
||||
|
||||
# check production state is Confirmed
|
||||
self.assertEqual(production_product_6.state, 'confirmed')
|
||||
|
||||
# Check procurement for product 4 created or not.
|
||||
# Check it created a purchase order
|
||||
|
||||
move_raw_product4 = production_product_6.move_raw_ids.filtered(lambda x: x.product_id == self.product_4)
|
||||
produce_product_4 = self.env['mrp.production'].search([('product_id', '=', self.product_4.id),
|
||||
('move_dest_ids', '=', move_raw_product4[0].id)])
|
||||
# produce product
|
||||
self.assertEqual(produce_product_4.reservation_state, 'confirmed', "Consume material not available")
|
||||
|
||||
# Create production order
|
||||
# -------------------------
|
||||
# Product 4 96 Unit
|
||||
# Product2 48 Unit
|
||||
# ---------------------
|
||||
# Update Inventory
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_2.id,
|
||||
'inventory_quantity': 48,
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
}).action_apply_inventory()
|
||||
produce_product_4.action_assign()
|
||||
self.assertEqual(produce_product_4.product_qty, 96, "Wrong quantity of finish product.")
|
||||
self.assertEqual(produce_product_4.product_uom_id, self.uom_unit, "Wrong quantity of finish product.")
|
||||
self.assertEqual(produce_product_4.reservation_state, 'assigned', "Consume material not available")
|
||||
|
||||
# produce product4
|
||||
# ---------------
|
||||
|
||||
mo_form = Form(produce_product_4)
|
||||
mo_form.qty_producing = produce_product_4.product_qty
|
||||
produce_product_4 = mo_form.save()
|
||||
# Check procurement and Production state for product 4.
|
||||
produce_product_4.button_mark_done()
|
||||
self.assertEqual(produce_product_4.state, 'done', 'Production order should be in state done')
|
||||
|
||||
# Produce product 6
|
||||
# ------------------
|
||||
|
||||
# Update Inventory
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_2.id,
|
||||
'inventory_quantity': 12,
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
}).action_apply_inventory()
|
||||
production_product_6.action_assign()
|
||||
|
||||
# ------------------------------------
|
||||
|
||||
self.assertEqual(production_product_6.reservation_state, 'assigned', "Consume material not available")
|
||||
mo_form = Form(production_product_6)
|
||||
mo_form.qty_producing = production_product_6.product_qty
|
||||
production_product_6 = mo_form.save()
|
||||
# Check procurement and Production state for product 6.
|
||||
production_product_6.button_mark_done()
|
||||
self.assertEqual(production_product_6.state, 'done', 'Production order should be in state done')
|
||||
self.assertEqual(self.product_6.qty_available, 24, 'Wrong quantity available of finished product.')
|
||||
|
||||
def test_procurement_2(self):
|
||||
"""Check that a manufacturing order create the right procurements when the route are set on
|
||||
a parent category of a product"""
|
||||
# find a child category id
|
||||
all_categ_id = self.env['product.category'].search([('parent_id', '=', None)], limit=1)
|
||||
child_categ_id = self.env['product.category'].search([('parent_id', '=', all_categ_id.id)], limit=1)
|
||||
|
||||
# set the product of `self.bom_1` to this child category
|
||||
for bom_line_id in self.bom_1.bom_line_ids:
|
||||
# check that no routes are defined on the product
|
||||
self.assertEqual(len(bom_line_id.product_id.route_ids), 0)
|
||||
# set the category of the product to a child category
|
||||
bom_line_id.product_id.categ_id = child_categ_id
|
||||
|
||||
# set the MTO route to the parent category (all)
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
mto_route = self.warehouse.mto_pull_id.route_id
|
||||
mto_route.active = True
|
||||
mto_route.product_categ_selectable = True
|
||||
all_categ_id.write({'route_ids': [(6, 0, [mto_route.id])]})
|
||||
|
||||
# create MO, but check it raises error as components are in make to order and not everyone has
|
||||
with self.assertRaises(UserError):
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.product_4
|
||||
production_form.product_uom_id = self.product_4.uom_id
|
||||
production_form.product_qty = 1
|
||||
production_product_4 = production_form.save()
|
||||
production_product_4.action_confirm()
|
||||
|
||||
def test_procurement_3(self):
|
||||
warehouse = self.env['stock.warehouse'].search([], limit=1)
|
||||
warehouse.write({'reception_steps': 'three_steps'})
|
||||
warehouse.mto_pull_id.route_id.active = True
|
||||
self.env['stock.location']._parent_store_compute()
|
||||
warehouse.reception_route_id.rule_ids.filtered(
|
||||
lambda p: p.location_src_id == warehouse.wh_input_stock_loc_id and
|
||||
p.location_dest_id == warehouse.wh_qc_stock_loc_id).write({
|
||||
'procure_method': 'make_to_stock'
|
||||
})
|
||||
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Finished Product',
|
||||
'type': 'product',
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Component',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, warehouse.mto_pull_id.route_id.id)]
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(component, warehouse.wh_input_stock_loc_id, 100)
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': finished_product.id,
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component.id, 'product_qty': 1.0})
|
||||
]})
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = finished_product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 5
|
||||
mo_form.product_uom_id = finished_product.uom_id
|
||||
mo_form.location_src_id = warehouse.lot_stock_id
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
pickings = self.env['stock.picking'].search([('product_id', '=', component.id)])
|
||||
self.assertEqual(len(pickings), 2.0)
|
||||
picking_input_to_qc = pickings.filtered(lambda p: p.location_id == warehouse.wh_input_stock_loc_id)
|
||||
picking_qc_to_stock = pickings - picking_input_to_qc
|
||||
self.assertTrue(picking_input_to_qc)
|
||||
self.assertTrue(picking_qc_to_stock)
|
||||
picking_input_to_qc.action_assign()
|
||||
self.assertEqual(picking_input_to_qc.state, 'assigned')
|
||||
picking_input_to_qc.move_line_ids.write({'qty_done': 5.0})
|
||||
picking_input_to_qc._action_done()
|
||||
picking_qc_to_stock.action_assign()
|
||||
self.assertEqual(picking_qc_to_stock.state, 'assigned')
|
||||
picking_qc_to_stock.move_line_ids.write({'qty_done': 3.0})
|
||||
picking_qc_to_stock.with_context(skip_backorder=True, picking_ids_not_to_backorder=picking_qc_to_stock.ids).button_validate()
|
||||
self.assertEqual(picking_qc_to_stock.state, 'done')
|
||||
mo.action_assign()
|
||||
self.assertEqual(mo.move_raw_ids.reserved_availability, 3.0)
|
||||
produce_form = Form(mo)
|
||||
produce_form.qty_producing = 3.0
|
||||
mo = produce_form.save()
|
||||
self.assertEqual(mo.move_raw_ids.quantity_done, 3.0)
|
||||
picking_qc_to_stock.move_line_ids.qty_done = 5.0
|
||||
self.assertEqual(mo.move_raw_ids.reserved_availability, 5.0)
|
||||
self.assertEqual(mo.move_raw_ids.quantity_done, 3.0)
|
||||
|
||||
def test_link_date_mo_moves(self):
|
||||
""" Check link of shedule date for manufaturing with date stock move."""
|
||||
|
||||
# create a product with manufacture route
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'AAA',
|
||||
'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))]
|
||||
})
|
||||
|
||||
component_1 = self.env['product.product'].create({
|
||||
'name': 'component',
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_1.id,
|
||||
'product_tmpl_id': product_1.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component_1.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
# create a move for product_1 from stock to output and reserve to trigger the
|
||||
# rule
|
||||
move_dest = self.env['stock.move'].create({
|
||||
'name': 'move_orig',
|
||||
'product_id': product_1.id,
|
||||
'product_uom': self.ref('uom.product_uom_unit'),
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_output'),
|
||||
'product_uom_qty': 10,
|
||||
'procure_method': 'make_to_order'
|
||||
})
|
||||
|
||||
move_dest._action_confirm()
|
||||
mo = self.env['mrp.production'].search([
|
||||
('product_id', '=', product_1.id),
|
||||
('state', '=', 'confirmed')
|
||||
])
|
||||
|
||||
self.assertAlmostEqual(mo.move_finished_ids.date, mo.move_raw_ids.date + timedelta(hours=1), delta=timedelta(seconds=1))
|
||||
|
||||
self.assertEqual(len(mo), 1, 'the manufacture order is not created')
|
||||
|
||||
mo_form = Form(mo)
|
||||
self.assertEqual(mo_form.product_qty, 10, 'the quantity to produce is not good relative to the move')
|
||||
|
||||
mo = mo_form.save()
|
||||
|
||||
# Confirming mo create finished move
|
||||
move_orig = self.env['stock.move'].search([
|
||||
('move_dest_ids', 'in', move_dest.ids)
|
||||
], limit=1)
|
||||
|
||||
self.assertEqual(len(move_orig), 1, 'the move orig is not created')
|
||||
self.assertEqual(move_orig.product_qty, 10, 'the quantity to produce is not good relative to the move')
|
||||
|
||||
new_sheduled_date = fields.Datetime.to_datetime(mo.date_planned_start) + timedelta(days=5)
|
||||
mo.date_planned_start = new_sheduled_date
|
||||
|
||||
self.assertAlmostEqual(mo.move_raw_ids.date, mo.date_planned_start, delta=timedelta(seconds=1))
|
||||
self.assertAlmostEqual(mo.move_finished_ids.date, mo.date_planned_finished, delta=timedelta(seconds=1))
|
||||
|
||||
def test_finished_move_cancellation(self):
|
||||
"""Check state of finished move on cancellation of raw moves. """
|
||||
product_bottle = self.env['product.product'].create({
|
||||
'name': 'Plastic Bottle',
|
||||
'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))]
|
||||
})
|
||||
|
||||
component_mold = self.env['product.product'].create({
|
||||
'name': 'Plastic Mold',
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_bottle.id,
|
||||
'product_tmpl_id': product_bottle.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component_mold.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
move_dest = self.env['stock.move'].create({
|
||||
'name': 'move_bottle',
|
||||
'product_id': product_bottle.id,
|
||||
'product_uom': self.ref('uom.product_uom_unit'),
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_output'),
|
||||
'product_uom_qty': 10,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
|
||||
move_dest._action_confirm()
|
||||
mo = self.env['mrp.production'].search([
|
||||
('product_id', '=', product_bottle.id),
|
||||
('state', '=', 'confirmed')
|
||||
])
|
||||
mo.move_raw_ids[0]._action_cancel()
|
||||
self.assertEqual(mo.state, 'cancel', 'Manufacturing order should be cancelled.')
|
||||
self.assertEqual(mo.move_finished_ids[0].state, 'cancel', 'Finished move should be cancelled if mo is cancelled.')
|
||||
self.assertEqual(mo.move_dest_ids[0].state, 'waiting', 'Destination move should not be cancelled if prapogation cancel is False on manufacturing rule.')
|
||||
|
||||
def test_procurement_with_empty_bom(self):
|
||||
"""Ensure that a procurement request using a product with an empty BoM
|
||||
will create an empty MO in draft state that can be completed afterwards.
|
||||
"""
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
|
||||
route_mto = self.warehouse.mto_pull_id.route_id.id
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Clafoutis',
|
||||
'route_ids': [(6, 0, [route_manufacture, route_mto])]
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product.id,
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
})
|
||||
move_dest = self.env['stock.move'].create({
|
||||
'name': 'Customer MTO Move',
|
||||
'product_id': product.id,
|
||||
'product_uom': self.ref('uom.product_uom_unit'),
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_output'),
|
||||
'product_uom_qty': 10,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
move_dest._action_confirm()
|
||||
|
||||
production = self.env['mrp.production'].search([('product_id', '=', product.id)])
|
||||
self.assertTrue(production)
|
||||
self.assertFalse(production.move_raw_ids)
|
||||
self.assertEqual(production.state, 'draft')
|
||||
|
||||
comp1 = self.env['product.product'].create({
|
||||
'name': 'egg',
|
||||
})
|
||||
move_values = production._get_move_raw_values(comp1, 40.0, self.env.ref('uom.product_uom_unit'))
|
||||
self.env['stock.move'].create(move_values)
|
||||
|
||||
production.action_confirm()
|
||||
produce_form = Form(production)
|
||||
produce_form.qty_producing = production.product_qty
|
||||
production = produce_form.save()
|
||||
production.button_mark_done()
|
||||
|
||||
move_dest._action_assign()
|
||||
self.assertEqual(move_dest.reserved_availability, 10.0)
|
||||
|
||||
def test_auto_assign(self):
|
||||
""" When auto reordering rule exists, check for when:
|
||||
1. There is not enough of a manufactured product to assign (reserve for) a picking => auto-create 1st MO
|
||||
2. There is not enough of a manufactured component to assign the created MO => auto-create 2nd MO
|
||||
3. Add an extra manufactured component (not in stock) to 1st MO => auto-create 3rd MO
|
||||
4. When 2nd MO is completed => auto-assign to 1st MO
|
||||
5. When 1st MO is completed => auto-assign to picking
|
||||
6. Additionally check that a MO that has component in stock auto-reserves when MO is confirmed (since default setting = 'at_confirm')"""
|
||||
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
route_manufacture = self.warehouse.manufacture_pull_id.route_id
|
||||
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'Cake',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id])]
|
||||
})
|
||||
product_2 = self.env['product.product'].create({
|
||||
'name': 'Cake Mix',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id])]
|
||||
})
|
||||
product_3 = self.env['product.product'].create({
|
||||
'name': 'Flour',
|
||||
'type': 'consu',
|
||||
})
|
||||
|
||||
bom1 = self.env['mrp.bom'].create({
|
||||
'product_id': product_1.id,
|
||||
'product_tmpl_id': product_1.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_2.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_2.id,
|
||||
'product_tmpl_id': product_2.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_3.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
# extra manufactured component added to 1st MO after it is already confirmed
|
||||
product_4 = self.env['product.product'].create({
|
||||
'name': 'Flavor Enchancer',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id])]
|
||||
})
|
||||
product_5 = self.env['product.product'].create({
|
||||
'name': 'MSG',
|
||||
'type': 'consu',
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_4.id,
|
||||
'product_tmpl_id': product_4.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_5.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
# setup auto orderpoints (reordering rules)
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Cake RR',
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'product_id': product_1.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 5,
|
||||
})
|
||||
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Cake Mix RR',
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'product_id': product_2.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 5,
|
||||
})
|
||||
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Flavor Enchancer RR',
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'product_id': product_4.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 5,
|
||||
})
|
||||
|
||||
# create picking output to trigger creating MO for reordering product_1
|
||||
pick_output = self.env['stock.picking'].create({
|
||||
'name': 'Cake Delivery Order',
|
||||
'picking_type_id': self.ref('stock.picking_type_out'),
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
'move_ids': [(0, 0, {
|
||||
'name': '/',
|
||||
'product_id': product_1.id,
|
||||
'product_uom': product_1.uom_id.id,
|
||||
'product_uom_qty': 10.00,
|
||||
'procure_method': 'make_to_stock',
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
})],
|
||||
})
|
||||
pick_output.action_confirm() # should trigger orderpoint to create and confirm 1st MO
|
||||
pick_output.action_assign()
|
||||
|
||||
mo = self.env['mrp.production'].search([
|
||||
('product_id', '=', product_1.id),
|
||||
('state', '=', 'confirmed')
|
||||
])
|
||||
|
||||
self.assertEqual(len(mo), 1, "Manufacture order was not automatically created")
|
||||
mo.action_assign()
|
||||
mo.is_locked = False
|
||||
self.assertEqual(mo.move_raw_ids.reserved_availability, 0, "No components should be reserved yet")
|
||||
self.assertEqual(mo.product_qty, 15, "Quantity to produce should be picking demand + reordering rule max qty")
|
||||
|
||||
# 2nd MO for product_2 should have been created and confirmed when 1st MO for product_1 was confirmed
|
||||
mo2 = self.env['mrp.production'].search([
|
||||
('product_id', '=', product_2.id),
|
||||
('state', '=', 'confirmed')
|
||||
])
|
||||
|
||||
self.assertEqual(len(mo2), 1, 'Second manufacture order was not created')
|
||||
self.assertEqual(mo2.product_qty, 20, "Quantity to produce should be MO's 'to consume' qty + reordering rule max qty")
|
||||
mo2_form = Form(mo2)
|
||||
mo2_form.qty_producing = 20
|
||||
mo2 = mo2_form.save()
|
||||
mo2.button_mark_done()
|
||||
|
||||
self.assertEqual(mo.move_raw_ids.reserved_availability, 15, "Components should have been auto-reserved")
|
||||
|
||||
# add new component to 1st MO
|
||||
mo_form = Form(mo)
|
||||
with mo_form.move_raw_ids.new() as line:
|
||||
line.product_id = product_4
|
||||
line.product_uom_qty = 1
|
||||
mo_form.save() # should trigger orderpoint to create and confirm 3rd MO
|
||||
|
||||
mo3 = self.env['mrp.production'].search([
|
||||
('product_id', '=', product_4.id),
|
||||
('state', '=', 'confirmed')
|
||||
])
|
||||
|
||||
self.assertEqual(len(mo3), 1, 'Third manufacture order for added component was not created')
|
||||
self.assertEqual(mo3.product_qty, 6, "Quantity to produce should be 1 + reordering rule max qty")
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo.move_raw_ids.quantity_done = 15
|
||||
mo_form.qty_producing = 15
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
self.assertEqual(pick_output.move_ids_without_package.reserved_availability, 10, "Completed products should have been auto-reserved in picking")
|
||||
|
||||
# make sure next MO auto-reserves components now that they are in stock since
|
||||
# default reservation_method = 'at_confirm'
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_1
|
||||
mo_form.bom_id = bom1
|
||||
mo_form.product_qty = 5
|
||||
mo_form.product_uom_id = product_1.uom_id
|
||||
mo_assign_at_confirm = mo_form.save()
|
||||
mo_assign_at_confirm.action_confirm()
|
||||
|
||||
self.assertEqual(mo_assign_at_confirm.move_raw_ids.reserved_availability, 5, "Components should have been auto-reserved")
|
||||
|
||||
def test_check_update_qty_mto_chain(self):
|
||||
""" Simulate a mto chain with a manufacturing order. Updating the
|
||||
initial demand should also impact the initial move but not the
|
||||
linked manufacturing order.
|
||||
"""
|
||||
def create_run_procurement(product, product_qty, values=None):
|
||||
if not values:
|
||||
values = {
|
||||
'warehouse_id': picking_type_out.warehouse_id,
|
||||
'action': 'pull_push',
|
||||
'group_id': procurement_group,
|
||||
}
|
||||
return self.env['procurement.group'].run([self.env['procurement.group'].Procurement(
|
||||
product, product_qty, self.uom_unit, vendor.property_stock_customer,
|
||||
product.name, '/', self.env.company, values)
|
||||
])
|
||||
|
||||
picking_type_out = self.env.ref('stock.picking_type_out')
|
||||
vendor = self.env['res.partner'].create({
|
||||
'name': 'Roger'
|
||||
})
|
||||
# This needs to be tried with MTO route activated
|
||||
self.env['stock.route'].browse(self.ref('stock.route_warehouse0_mto')).action_unarchive()
|
||||
# Define products requested for this BoM.
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'product',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('mrp.route_warehouse0_manufacture'))],
|
||||
'categ_id': self.env.ref('product.product_category_all').id
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'component',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product.id,
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'product_uom_id': product.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component.id, 'product_qty': 1}),
|
||||
]
|
||||
})
|
||||
|
||||
procurement_group = self.env['procurement.group'].create({
|
||||
'move_type': 'direct',
|
||||
'partner_id': vendor.id
|
||||
})
|
||||
# Create initial procurement that will generate the initial move and its picking.
|
||||
create_run_procurement(product, 10, {
|
||||
'group_id': procurement_group,
|
||||
'warehouse_id': picking_type_out.warehouse_id,
|
||||
'partner_id': vendor
|
||||
})
|
||||
customer_move = self.env['stock.move'].search([('group_id', '=', procurement_group.id)])
|
||||
manufacturing_order = self.env['mrp.production'].search([('product_id', '=', product.id)])
|
||||
self.assertTrue(manufacturing_order, 'No manufacturing order created.')
|
||||
|
||||
# Check manufacturing order data.
|
||||
self.assertEqual(manufacturing_order.product_qty, 10, 'The manufacturing order qty should be the same as the move.')
|
||||
|
||||
# Create procurement to decrease quantity in the initial move but not in the related MO.
|
||||
create_run_procurement(product, -5.00)
|
||||
self.assertEqual(customer_move.product_uom_qty, 5, 'The demand on the initial move should have been decreased when merged with the procurement.')
|
||||
self.assertEqual(manufacturing_order.product_qty, 10, 'The demand on the manufacturing order should not have been decreased.')
|
||||
|
||||
# Create procurement to increase quantity on the initial move and should create a new MO for the missing qty.
|
||||
create_run_procurement(product, 2.00)
|
||||
self.assertEqual(customer_move.product_uom_qty, 5, 'The demand on the initial move should not have been increased since it should be a new move.')
|
||||
self.assertEqual(manufacturing_order.product_qty, 10, 'The demand on the initial manufacturing order should not have been increased.')
|
||||
manufacturing_orders = self.env['mrp.production'].search([('product_id', '=', product.id)])
|
||||
self.assertEqual(len(manufacturing_orders), 2, 'A new MO should have been created for missing demand.')
|
||||
|
||||
def test_rr_with_dependance_between_bom(self):
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
route_mto = self.warehouse.mto_pull_id.route_id
|
||||
route_mto.active = True
|
||||
route_manufacture = self.warehouse.manufacture_pull_id.route_id
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id])]
|
||||
})
|
||||
product_2 = self.env['product.product'].create({
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id, route_mto.id])]
|
||||
})
|
||||
product_3 = self.env['product.product'].create({
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id])]
|
||||
})
|
||||
product_4 = self.env['product.product'].create({
|
||||
'name': 'Product C',
|
||||
'type': 'consu',
|
||||
})
|
||||
|
||||
op1 = self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Product A',
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'product_id': product_1.id,
|
||||
'product_min_qty': 1,
|
||||
'product_max_qty': 20,
|
||||
})
|
||||
|
||||
op2 = self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Product B',
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'product_id': product_3.id,
|
||||
'product_min_qty': 5,
|
||||
'product_max_qty': 50,
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_1.id,
|
||||
'product_tmpl_id': product_1.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': product_2.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_2.id,
|
||||
'product_tmpl_id': product_2.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': product_3.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_3.id,
|
||||
'product_tmpl_id': product_3.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': product_4.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
(op1 | op2)._procure_orderpoint_confirm()
|
||||
mo1 = self.env['mrp.production'].search([('product_id', '=', product_1.id)])
|
||||
mo3 = self.env['mrp.production'].search([('product_id', '=', product_3.id)])
|
||||
|
||||
self.assertEqual(len(mo1), 1)
|
||||
self.assertEqual(len(mo3), 1)
|
||||
self.assertEqual(mo1.product_qty, 20)
|
||||
self.assertEqual(mo3.product_qty, 50)
|
||||
|
||||
def test_several_boms_same_finished_product(self):
|
||||
"""
|
||||
Suppose a product with two BoMs, each one based on a different operation type
|
||||
This test ensures that, when running the scheduler, the generated MOs are based
|
||||
on the correct BoMs
|
||||
"""
|
||||
# Required for `picking_type_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('stock.group_adv_location')
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
|
||||
stock_location01 = warehouse.lot_stock_id
|
||||
stock_location02 = stock_location01.copy()
|
||||
|
||||
manu_operation01 = warehouse.manu_type_id
|
||||
manu_operation02 = manu_operation01.copy()
|
||||
with Form(manu_operation02) as form:
|
||||
form.name = 'Manufacturing 02'
|
||||
form.sequence_code = 'MO2'
|
||||
form.default_location_dest_id = stock_location02
|
||||
|
||||
manu_rule01 = warehouse.manufacture_pull_id
|
||||
manu_route = manu_rule01.route_id
|
||||
manu_rule02 = manu_rule01.copy()
|
||||
with Form(manu_rule02) as form:
|
||||
form.picking_type_id = manu_operation02
|
||||
manu_route.rule_ids = [(6, 0, (manu_rule01 + manu_rule02).ids)]
|
||||
|
||||
compo01, compo02, finished = self.env['product.product'].create([{
|
||||
'name': 'compo 01',
|
||||
'type': 'consu',
|
||||
}, {
|
||||
'name': 'compo 02',
|
||||
'type': 'consu',
|
||||
}, {
|
||||
'name': 'finished',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, manu_route.ids)],
|
||||
}])
|
||||
|
||||
bom01_form = Form(self.env['mrp.bom'])
|
||||
bom01_form.product_tmpl_id = finished.product_tmpl_id
|
||||
bom01_form.code = '01'
|
||||
bom01_form.picking_type_id = manu_operation01
|
||||
with bom01_form.bom_line_ids.new() as line:
|
||||
line.product_id = compo01
|
||||
bom01 = bom01_form.save()
|
||||
|
||||
bom02_form = Form(self.env['mrp.bom'])
|
||||
bom02_form.product_tmpl_id = finished.product_tmpl_id
|
||||
bom02_form.code = '02'
|
||||
bom02_form.picking_type_id = manu_operation02
|
||||
with bom02_form.bom_line_ids.new() as line:
|
||||
line.product_id = compo02
|
||||
bom02 = bom02_form.save()
|
||||
|
||||
self.env['stock.warehouse.orderpoint'].create([{
|
||||
'warehouse_id': warehouse.id,
|
||||
'location_id': stock_location01.id,
|
||||
'product_id': finished.id,
|
||||
'product_min_qty': 1,
|
||||
'product_max_qty': 1,
|
||||
}, {
|
||||
'warehouse_id': warehouse.id,
|
||||
'location_id': stock_location02.id,
|
||||
'product_id': finished.id,
|
||||
'product_min_qty': 2,
|
||||
'product_max_qty': 2,
|
||||
}])
|
||||
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
|
||||
mos = self.env['mrp.production'].search([('product_id', '=', finished.id)], order='origin')
|
||||
self.assertRecordValues(mos, [
|
||||
{'product_qty': 1, 'bom_id': bom01.id, 'picking_type_id': manu_operation01.id, 'location_dest_id': stock_location01.id},
|
||||
{'product_qty': 2, 'bom_id': bom02.id, 'picking_type_id': manu_operation02.id, 'location_dest_id': stock_location02.id},
|
||||
])
|
||||
|
||||
def test_pbm_and_additionnal_components(self):
|
||||
"""
|
||||
2-steps manufacturring.
|
||||
When adding a new component to a confirmed MO, it should add an SM in
|
||||
the PBM picking. Also, it should be possible to define the to-consume
|
||||
qty of the new line even if the MO is locked
|
||||
"""
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
warehouse.manufacture_steps = 'pbm'
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_4
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
if not mo.is_locked:
|
||||
mo.action_toggle_is_locked()
|
||||
|
||||
with Form(mo) as mo_form:
|
||||
with mo_form.move_raw_ids.new() as raw_line:
|
||||
raw_line.product_id = self.product_2
|
||||
raw_line.product_uom_qty = 2.0
|
||||
|
||||
move_vals = mo._get_move_raw_values(self.product_3, 0, self.product_3.uom_id)
|
||||
mo.move_raw_ids = [(0, 0, move_vals)]
|
||||
mo.move_raw_ids[-1].product_uom_qty = 3.0
|
||||
|
||||
expected_vals = [
|
||||
{'product_id': self.product_1.id, 'product_uom_qty': 1.0},
|
||||
{'product_id': self.product_2.id, 'product_uom_qty': 2.0},
|
||||
{'product_id': self.product_3.id, 'product_uom_qty': 3.0},
|
||||
]
|
||||
self.assertRecordValues(mo.move_raw_ids, expected_vals)
|
||||
self.assertRecordValues(mo.picking_ids.move_ids, expected_vals)
|
||||
269
odoo-bringout-oca-ocb-mrp/mrp/tests/test_smp.py
Normal file
269
odoo-bringout-oca-ocb-mrp/mrp/tests/test_smp.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import Form
|
||||
from odoo import Command
|
||||
|
||||
|
||||
class TestMrpSerialMassProduce(TestMrpCommon):
|
||||
|
||||
def test_smp_serial(self):
|
||||
"""Create a MO for a product not tracked by serial number.
|
||||
The smp wizard should not open.
|
||||
"""
|
||||
mo = self.generate_mo()[0]
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
res = mo.action_serial_mass_produce_wizard()
|
||||
self.assertFalse(res)
|
||||
|
||||
def test_smp_produce_all(self):
|
||||
"""Create a MO for a product tracked by serial number.
|
||||
Open the smp wizard, generate all serial numbers to produce all quantities.
|
||||
"""
|
||||
mo = self.generate_mo(tracking_final='serial')[0]
|
||||
count = mo.product_qty
|
||||
# Make some stock and reserve
|
||||
for product in mo.move_raw_ids.product_id:
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': 100,
|
||||
'location_id': mo.location_src_id.id,
|
||||
})._apply_inventory()
|
||||
mo.action_assign()
|
||||
# Open the wizard
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
# Let the wizard generate all serial numbers
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = count
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to apply generated serial numbers
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().apply()
|
||||
# Initial MO should have a backorder-sequenced name and be in to_close state
|
||||
self.assertTrue("-001" in mo.name)
|
||||
self.assertEqual(mo.state, "to_close")
|
||||
# Each generated serial number should have its own mo
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), count)
|
||||
# Check generated serial numbers
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.lot_producing_id.mapped('name'), ["sn#1", "sn#2", "sn#3", "sn#4", "sn#5"])
|
||||
|
||||
def test_smp_produce_all_but_one(self):
|
||||
"""Create a MO for a product tracked by serial number.
|
||||
Open the smp wizard, generate all but one serial numbers and create a back order.
|
||||
"""
|
||||
mo = self.generate_mo(tracking_final='serial')[0]
|
||||
count = mo.product_qty
|
||||
# Make some stock and reserve
|
||||
for product in mo.move_raw_ids.product_id:
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': 100,
|
||||
'location_id': mo.location_src_id.id,
|
||||
})._apply_inventory()
|
||||
mo.action_assign()
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = count - 1
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to create backorder (applying generated serial numbers)
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().create_backorder()
|
||||
# Last MO in sequence is the backorder
|
||||
bo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(bo.backorder_sequence, count)
|
||||
self.assertEqual(bo.state, "confirmed")
|
||||
|
||||
def test_smp_produce_complex(self):
|
||||
"""Create a MO for a product tracked by serial number
|
||||
and with complex components (serial and multiple lots).
|
||||
Open the smp wizard, generate all serial numbers to produce all quantities.
|
||||
Check lot splitting.
|
||||
"""
|
||||
mo, dummy, dummy, product_to_use_1, product_to_use_2 = self.generate_mo(tracking_final='serial', tracking_base_1='lot', tracking_base_2='serial', qty_final=3, qty_base_1=2, qty_base_2=1)
|
||||
count = mo.product_qty
|
||||
# Make some stock and reserve
|
||||
for _ in range(2): # 2 lots of 3 to satisfy the need and check lot splitting
|
||||
lot = self.env['stock.lot'].create({
|
||||
'product_id': product_to_use_1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product_to_use_1.id,
|
||||
'inventory_quantity': 3,
|
||||
'location_id': mo.location_src_id.id,
|
||||
'lot_id': lot.id,
|
||||
})._apply_inventory()
|
||||
for _ in range(3): # 3 serial numbers
|
||||
lot = self.env['stock.lot'].create({
|
||||
'product_id': product_to_use_2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product_to_use_2.id,
|
||||
'inventory_quantity': 1,
|
||||
'location_id': mo.location_src_id.id,
|
||||
'lot_id': lot.id,
|
||||
})._apply_inventory()
|
||||
mo.action_assign()
|
||||
# Open the wizard
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
# Let the wizard generate all serial numbers
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = count
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to apply generated serial numbers
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().apply()
|
||||
# 1st & 3rd MO in sequence should have only 1 move lines (1 lot) for product_to_use_1 (2nd in bom)
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids[0].move_raw_ids[1].move_lines_count, 1)
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids[2].move_raw_ids[1].move_lines_count, 1)
|
||||
# 2nd MO should have 2 move lines (2 different lots) for product_to_use_1
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids[1].move_raw_ids[1].move_lines_count, 2)
|
||||
|
||||
def test_mass_produce_with_tracked_product(self):
|
||||
"""
|
||||
Check that we can mass produce a tracked product.
|
||||
"""
|
||||
tracked_product = self.env['product.product'].create({
|
||||
'name': 'Tracked Product',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Component',
|
||||
'type': 'product',
|
||||
})
|
||||
byproduct = self.env['product.product'].create({
|
||||
'name': 'Byproduct',
|
||||
'type': 'product',
|
||||
})
|
||||
# create a BoM
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': tracked_product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_line_ids': [Command.create({
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
'byproduct_ids': [Command.create({
|
||||
'product_id': byproduct.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
})
|
||||
sn_1 = self.env['stock.lot'].create({
|
||||
'name': 'SN1',
|
||||
'product_id': tracked_product.id,
|
||||
})
|
||||
sn_2 = self.env['stock.lot'].create({
|
||||
'name': 'SN2',
|
||||
'product_id': tracked_product.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(tracked_product, self.stock_location_14, 1, lot_id=sn_1)
|
||||
self.env['stock.quant']._update_available_quantity(tracked_product, self.stock_location_14, 1, lot_id=sn_2)
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location_14, 10)
|
||||
# create an MO to use the tracked product available in stock
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_1
|
||||
mo_form.product_qty = 2
|
||||
mo_form.product_uom_id = component.uom_id
|
||||
# use tracked as component
|
||||
with mo_form.move_raw_ids.new() as move:
|
||||
move.name = tracked_product.name
|
||||
move.product_id = tracked_product
|
||||
move.product_uom_qty = 2
|
||||
move.product_uom = tracked_product.uom_id
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
mo.qty_producing = 2
|
||||
mo.move_raw_ids.move_line_ids.write({'qty_done': 1})
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
# create a Mo to produce 2 units of tracked product
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = tracked_product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 2
|
||||
mo_form.product_uom_id = tracked_product.uom_id
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
|
||||
mo.action_assign()
|
||||
# Open the wizard
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
# Let the wizard generate all serial numbers
|
||||
wizard.next_serial_number = "sn#3"
|
||||
wizard.next_serial_count = 2
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to apply generated serial numbers
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().apply()
|
||||
# Initial MO should have a backorder-sequenced name and be in to_close state
|
||||
self.assertTrue("-001" in mo.name)
|
||||
self.assertEqual(mo.state, "to_close")
|
||||
# Each generated serial number should have its own mo
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 2)
|
||||
# Check generated serial numbers
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.lot_producing_id.mapped('name'), ["sn#3", "sn#4"])
|
||||
#check byproduct quantity
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.move_byproduct_ids.mapped('quantity_done'), [1, 1])
|
||||
# check the component quantity
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.move_raw_ids.mapped('quantity_done'), [1, 1])
|
||||
# Mark the MOs as done
|
||||
mo.procurement_group_id.mrp_production_ids.button_mark_done()
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped('state'), ['done', 'done'])
|
||||
|
||||
def test_smp_produce_with_consumable_component(self):
|
||||
"""Create a MO for a product tracked by serial number with a consumable component.
|
||||
Open the smp wizard, You should be able to generate all serial numbers.
|
||||
BoM:
|
||||
- 1x final product (tracked by serial number)
|
||||
components:
|
||||
- 2 x (storable)
|
||||
- 4 x (consumable)
|
||||
- Create a MO with 12 final products to produce.
|
||||
- update the component quantity to 100
|
||||
"""
|
||||
self.bom_1.product_id.uom_id = self.ref('uom.product_uom_unit')
|
||||
self.bom_1.product_id.tracking = 'serial'
|
||||
self.bom_1.product_qty = 1
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_1
|
||||
mo_form.product_qty = 12
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(mo.move_raw_ids.mapped(lambda l: l.product_qty), [24, 48])
|
||||
self.assertEqual(mo.move_raw_ids[1].product_id.type, 'consu', 'The second component should be consumable')
|
||||
mo.move_raw_ids[1].product_uom_qty = 100
|
||||
|
||||
# Make some stock and reserve for storable component
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': mo.move_raw_ids[0].product_id.id,
|
||||
'inventory_quantity': 24,
|
||||
'location_id': mo.location_src_id.id,
|
||||
})._apply_inventory()
|
||||
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
|
||||
# Open the wizard
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
# Let the wizard generate all serial numbers
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = mo.product_qty
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to apply generated serial numbers
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().apply()
|
||||
# Initial MO should have a backorder-sequenced name and be in to_close state
|
||||
self.assertTrue("-001" in mo.name)
|
||||
self.assertEqual(mo.state, "to_close")
|
||||
# Each generated serial number should have its own mo
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 12)
|
||||
524
odoo-bringout-oca-ocb-mrp/mrp/tests/test_stock.py
Normal file
524
odoo-bringout-oca-ocb-mrp/mrp/tests/test_stock.py
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from odoo import Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestWarehouseMrp(common.TestMrpCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
unit = cls.env.ref("uom.product_uom_unit")
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
cls.depot_location = cls.env['stock.location'].create({
|
||||
'name': 'Depot',
|
||||
'usage': 'internal',
|
||||
'location_id': cls.stock_location.id,
|
||||
})
|
||||
cls.env["stock.putaway.rule"].create({
|
||||
"location_in_id": cls.stock_location.id,
|
||||
"location_out_id": cls.depot_location.id,
|
||||
'category_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.env['stock.quant'].create({
|
||||
'location_id': cls.stock_location_14.id,
|
||||
'product_id': cls.graphics_card.id,
|
||||
'inventory_quantity': 16.0
|
||||
}).action_apply_inventory()
|
||||
|
||||
cls.bom_laptop = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.laptop.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit.id,
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': cls.graphics_card.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit.id
|
||||
})],
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
def new_mo_laptop(self):
|
||||
form = Form(self.env['mrp.production'])
|
||||
form.product_id = self.laptop
|
||||
form.product_qty = 1
|
||||
form.bom_id = self.bom_laptop
|
||||
p = form.save()
|
||||
p.action_confirm()
|
||||
p.action_assign()
|
||||
return p
|
||||
|
||||
def test_manufacturing_route(self):
|
||||
warehouse_1_stock_manager = self.warehouse_1.with_user(self.user_stock_manager)
|
||||
manu_rule = self.env['stock.rule'].search([
|
||||
('action', '=', 'manufacture'),
|
||||
('warehouse_id', '=', self.warehouse_1.id)])
|
||||
self.assertEqual(self.warehouse_1.manufacture_pull_id, manu_rule)
|
||||
manu_route = manu_rule.route_id
|
||||
self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
|
||||
warehouse_1_stock_manager.write({
|
||||
'manufacture_to_resupply': False
|
||||
})
|
||||
self.assertFalse(self.warehouse_1.manufacture_pull_id.active)
|
||||
self.assertFalse(self.warehouse_1.manu_type_id.active)
|
||||
self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes())
|
||||
warehouse_1_stock_manager.write({
|
||||
'manufacture_to_resupply': True
|
||||
})
|
||||
manu_rule = self.env['stock.rule'].search([
|
||||
('action', '=', 'manufacture'),
|
||||
('warehouse_id', '=', self.warehouse_1.id)])
|
||||
self.assertEqual(self.warehouse_1.manufacture_pull_id, manu_rule)
|
||||
self.assertTrue(self.warehouse_1.manu_type_id.active)
|
||||
self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
|
||||
|
||||
def test_manufacturing_scrap(self):
|
||||
"""
|
||||
Testing to do a scrap of consumed material.
|
||||
"""
|
||||
|
||||
# Update demo products
|
||||
(self.product_4 | self.product_2).write({
|
||||
'tracking': 'lot',
|
||||
})
|
||||
|
||||
# Update Bill Of Material to remove product with phantom bom.
|
||||
self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
|
||||
|
||||
# Create Inventory Adjustment For Stick and Stone Tools with lot.
|
||||
lot_product_4 = self.env['stock.lot'].create({
|
||||
'name': '0000000000001',
|
||||
'product_id': self.product_4.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_product_2 = self.env['stock.lot'].create({
|
||||
'name': '0000000000002',
|
||||
'product_id': self.product_2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
# Inventory for Stick
|
||||
self.env['stock.quant'].create({
|
||||
'location_id': self.stock_location_14.id,
|
||||
'product_id': self.product_4.id,
|
||||
'inventory_quantity': 8,
|
||||
'lot_id': lot_product_4.id
|
||||
}).action_apply_inventory()
|
||||
|
||||
# Inventory for Stone Tools
|
||||
self.env['stock.quant'].create({
|
||||
'location_id': self.stock_location_14.id,
|
||||
'product_id': self.product_2.id,
|
||||
'inventory_quantity': 12,
|
||||
'lot_id': lot_product_2.id
|
||||
}).action_apply_inventory()
|
||||
|
||||
#Create Manufacturing order.
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.product_6
|
||||
production_form.bom_id = self.bom_3
|
||||
production_form.product_qty = 12
|
||||
production_form.product_uom_id = self.product_6.uom_id
|
||||
production_3 = production_form.save()
|
||||
production_3.action_confirm()
|
||||
production_3.action_assign()
|
||||
|
||||
# Check Manufacturing order's availability.
|
||||
self.assertEqual(production_3.reservation_state, 'assigned', "Production order's availability should be Available.")
|
||||
|
||||
location_id = production_3.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) and production_3.location_src_id.id or production_3.location_dest_id.id,
|
||||
|
||||
# Scrap Product Wood without lot to check assert raise ?.
|
||||
scrap_id = self.env['stock.scrap'].with_context(active_model='mrp.production', active_id=production_3.id).create({'product_id': self.product_2.id, 'scrap_qty': 1.0, 'product_uom_id': self.product_2.uom_id.id, 'location_id': location_id, 'production_id': production_3.id})
|
||||
with self.assertRaises(UserError):
|
||||
scrap_id.do_scrap()
|
||||
|
||||
# Scrap Product Wood with lot.
|
||||
scrap_id = self.env['stock.scrap'].with_context(active_model='mrp.production', active_id=production_3.id).create({'product_id': self.product_2.id, 'scrap_qty': 1.0, 'product_uom_id': self.product_2.uom_id.id, 'location_id': location_id, 'lot_id': lot_product_2.id, 'production_id': production_3.id})
|
||||
scrap_id.do_scrap()
|
||||
scrap_move = scrap_id.move_id
|
||||
|
||||
self.assertTrue(scrap_move.raw_material_production_id)
|
||||
self.assertTrue(scrap_move.scrapped)
|
||||
self.assertEqual(scrap_move.location_dest_id, scrap_id.scrap_location_id)
|
||||
self.assertEqual(scrap_move.price_unit, scrap_move.product_id.standard_price)
|
||||
|
||||
#Check scrap move is created for this production order.
|
||||
#TODO: should check with scrap objects link in between
|
||||
|
||||
# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.scrapped)
|
||||
# self.assertTrue(scrap_move, "There are no any scrap move created for production order.")
|
||||
|
||||
def test_putaway_after_manufacturing_3(self):
|
||||
""" This test checks a tracked manufactured product will go to location
|
||||
defined in putaway strategy when the production is recorded with
|
||||
product.produce wizard.
|
||||
"""
|
||||
self.laptop.tracking = 'serial'
|
||||
mo_laptop = self.new_mo_laptop()
|
||||
serial = self.env['stock.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id})
|
||||
|
||||
mo_form = Form(mo_laptop)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.lot_producing_id = serial
|
||||
mo_laptop = mo_form.save()
|
||||
mo_laptop.button_mark_done()
|
||||
|
||||
# We check if the laptop go in the depot and not in the stock
|
||||
move = mo_laptop.move_finished_ids
|
||||
location_dest = move.move_line_ids.location_dest_id
|
||||
self.assertEqual(location_dest.id, self.depot_location.id)
|
||||
self.assertNotEqual(location_dest.id, self.stock_location.id)
|
||||
|
||||
def test_backorder_unpacking(self):
|
||||
""" Test that movement of pack in backorder is correctly handled. """
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
warehouse.write({'manufacture_steps': 'pbm'})
|
||||
|
||||
self.product_1.type = 'product'
|
||||
self.env['stock.quant']._update_available_quantity(self.product_1, self.stock_location, 100)
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_4
|
||||
mo_form.product_qty = 100
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
package = self.env['stock.quant.package'].create({})
|
||||
|
||||
picking = mo.picking_ids
|
||||
picking.move_line_ids.write({
|
||||
'qty_done': 20,
|
||||
'result_package_id': package.id,
|
||||
})
|
||||
|
||||
res_dict = picking.button_validate()
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
backorder = picking.backorder_ids
|
||||
backorder.move_line_ids.qty_done = 80
|
||||
backorder.button_validate()
|
||||
|
||||
self.assertEqual(picking.state, 'done')
|
||||
self.assertEqual(backorder.state, 'done')
|
||||
self.assertEqual(mo.move_raw_ids.move_line_ids.mapped('reserved_qty'), [20, 80])
|
||||
|
||||
def test_unarchive_mto_route_active_needed_rules_only(self):
|
||||
""" Ensure that activating a route will activate only its relevant rules.
|
||||
Here, unarchiving the MTO route shouldn't active pull rule for the Pre-Production
|
||||
location if manufacture is in 1 step since this location is archived.
|
||||
"""
|
||||
|
||||
self.env.user.groups_id += self.env.ref('stock.group_adv_location')
|
||||
mto_route = self.env.ref('stock.route_warehouse0_mto')
|
||||
|
||||
# initially 'WH: Stock → Pre-Production (MTO)' is inactive and not shown in MTO route.
|
||||
self.assertEqual(self.warehouse_1.manufacture_steps, 'mrp_one_step')
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
|
||||
self.assertFalse(mto_route.active)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
|
||||
|
||||
# Activate the MTO route and still 'WH: Stock → Pre-Production (MTO)' is not shown in MTO route.
|
||||
mto_route.active = True
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
|
||||
|
||||
# Change MRP steps mrp_one_step to pbm_sam and now that rule is shown in mto route.
|
||||
self.warehouse_1.manufacture_steps = 'pbm_sam'
|
||||
self.assertTrue(self.warehouse_1.pbm_mto_pull_id.active)
|
||||
self.assertTrue(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
|
||||
self.assertIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
|
||||
|
||||
# Revert to mrp_one_step MRP and confirm rules visibility is updated correctly
|
||||
self.warehouse_1.manufacture_steps = 'mrp_one_step'
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
|
||||
|
||||
|
||||
class TestKitPicking(common.TestMrpCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
def create_product(name):
|
||||
p = Form(cls.env['product.product'])
|
||||
p.name = name
|
||||
p.detailed_type = 'product'
|
||||
return p.save()
|
||||
|
||||
# Create a kit 'kit_parent' :
|
||||
# ---------------------------
|
||||
#
|
||||
# kit_parent --|- kit_2 x2 --|- component_d x1
|
||||
# | |- kit_1 x2 -------|- component_a x2
|
||||
# | |- component_b x1
|
||||
# | |- component_c x3
|
||||
# |
|
||||
# |- kit_3 x1 --|- component_f x1
|
||||
# | |- component_g x2
|
||||
# |
|
||||
# |- component_e x1
|
||||
# Creating all components
|
||||
component_a = create_product('Comp A')
|
||||
component_b = create_product('Comp B')
|
||||
component_c = create_product('Comp C')
|
||||
component_d = create_product('Comp D')
|
||||
component_e = create_product('Comp E')
|
||||
component_f = create_product('Comp F')
|
||||
component_g = create_product('Comp G')
|
||||
# Creating all kits
|
||||
kit_1 = create_product('Kit 1')
|
||||
kit_2 = create_product('Kit 2')
|
||||
kit_3 = create_product('kit 3')
|
||||
cls.kit_parent = create_product('Kit Parent')
|
||||
# Linking the kits and the components via some 'phantom' BoMs
|
||||
bom_kit_1 = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_1.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
BomLine = cls.env['mrp.bom.line']
|
||||
BomLine.create({
|
||||
'product_id': component_a.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_1.id})
|
||||
BomLine.create({
|
||||
'product_id': component_b.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_1.id})
|
||||
BomLine.create({
|
||||
'product_id': component_c.id,
|
||||
'product_qty': 3.0,
|
||||
'bom_id': bom_kit_1.id})
|
||||
bom_kit_2 = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_2.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
BomLine.create({
|
||||
'product_id': component_d.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_2.id})
|
||||
BomLine.create({
|
||||
'product_id': kit_1.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_2.id})
|
||||
bom_kit_parent = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.kit_parent.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
BomLine.create({
|
||||
'product_id': component_e.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
BomLine.create({
|
||||
'product_id': kit_2.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
bom_kit_3 = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_3.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
BomLine.create({
|
||||
'product_id': component_f.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_3.id})
|
||||
BomLine.create({
|
||||
'product_id': component_g.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_3.id})
|
||||
BomLine.create({
|
||||
'product_id': kit_3.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
|
||||
# We create an 'immediate transfer' receipt for x3 kit_parent
|
||||
cls.test_partner = cls.env['res.partner'].create({
|
||||
'name': 'Notthat Guyagain',
|
||||
})
|
||||
cls.test_supplier = cls.env['stock.location'].create({
|
||||
'name': 'supplier',
|
||||
'usage': 'supplier',
|
||||
'location_id': cls.env.ref('stock.stock_location_stock').id,
|
||||
})
|
||||
|
||||
cls.expected_quantities = {
|
||||
component_a: 24,
|
||||
component_b: 12,
|
||||
component_c: 36,
|
||||
component_d: 6,
|
||||
component_e: 3,
|
||||
component_f: 3,
|
||||
component_g: 6
|
||||
}
|
||||
|
||||
def test_kit_immediate_transfer(self):
|
||||
""" Make sure a kit is split in the corrects quantity_done by components in case of an
|
||||
immediate transfer.
|
||||
"""
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.test_supplier.id,
|
||||
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
|
||||
'partner_id': self.test_partner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
'immediate_transfer': True
|
||||
})
|
||||
move_receipt_1 = self.env['stock.move'].create({
|
||||
'name': self.kit_parent.name,
|
||||
'product_id': self.kit_parent.id,
|
||||
'quantity_done': 3,
|
||||
'product_uom': self.kit_parent.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
'location_id': self.test_supplier.id,
|
||||
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
|
||||
})
|
||||
picking.button_validate()
|
||||
|
||||
# We check that the picking has the correct quantities after its move were splitted.
|
||||
self.assertEqual(len(picking.move_ids), 7)
|
||||
for move_line in picking.move_ids:
|
||||
self.assertEqual(move_line.quantity_done, self.expected_quantities[move_line.product_id])
|
||||
|
||||
def test_kit_planned_transfer(self):
|
||||
""" Make sure a kit is split in the corrects product_qty by components in case of a
|
||||
planned transfer.
|
||||
"""
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.test_supplier.id,
|
||||
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
|
||||
'partner_id': self.test_partner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
'immediate_transfer': False,
|
||||
})
|
||||
move_receipt_1 = self.env['stock.move'].create({
|
||||
'name': self.kit_parent.name,
|
||||
'product_id': self.kit_parent.id,
|
||||
'product_uom_qty': 3,
|
||||
'product_uom': self.kit_parent.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
'location_id': self.test_supplier.id,
|
||||
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
|
||||
})
|
||||
picking.action_confirm()
|
||||
|
||||
# We check that the picking has the correct quantities after its move were splitted.
|
||||
self.assertEqual(len(picking.move_ids), 7)
|
||||
for move_line in picking.move_ids:
|
||||
self.assertEqual(move_line.product_qty, self.expected_quantities[move_line.product_id])
|
||||
|
||||
def test_add_sml_with_kit_to_confirmed_picking(self):
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
customer_location = self.env.ref('stock.stock_location_customers')
|
||||
stock_location = warehouse.lot_stock_id
|
||||
in_type = warehouse.in_type_id
|
||||
|
||||
self.bom_4.type = 'phantom'
|
||||
kit = self.bom_4.product_id
|
||||
compo = self.bom_4.bom_line_ids.product_id
|
||||
product = self.env['product.product'].create({'name': 'Super Product', 'type': 'product'})
|
||||
|
||||
receipt = self.env['stock.picking'].create({
|
||||
'picking_type_id': in_type.id,
|
||||
'location_id': customer_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'move_ids': [(0, 0, {
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'location_id': customer_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
})]
|
||||
})
|
||||
receipt.action_confirm()
|
||||
|
||||
receipt.move_line_ids.qty_done = 1
|
||||
receipt.move_line_ids = [(0, 0, {
|
||||
'product_id': kit.id,
|
||||
'qty_done': 1,
|
||||
'product_uom_id': kit.uom_id.id,
|
||||
'location_id': customer_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
})]
|
||||
|
||||
receipt.button_validate()
|
||||
|
||||
self.assertEqual(receipt.state, 'done')
|
||||
self.assertRecordValues(receipt.move_ids, [
|
||||
{'product_id': product.id, 'quantity_done': 1, 'state': 'done'},
|
||||
{'product_id': compo.id, 'quantity_done': 1, 'state': 'done'},
|
||||
])
|
||||
|
||||
def test_move_line_aggregated_product_quantities_with_kit(self):
|
||||
""" Test the `stock.move.line` method `_get_aggregated_product_quantities`,
|
||||
who returns data used to print delivery slips, using kits.
|
||||
"""
|
||||
uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
kit, kit_component_1, kit_component_2, not_kit_1, not_kit_2 = self.env['product.product'].create([{
|
||||
'name': name,
|
||||
'type': 'product',
|
||||
'uom_id': uom_unit.id,
|
||||
} for name in ['Kit', 'Kit Component 1', 'Kit Component 2', 'Not Kit 1', 'Not Kit 2']])
|
||||
|
||||
bom_kit = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit.product_tmpl_id.id,
|
||||
'product_uom_id': kit.product_tmpl_id.uom_id.id,
|
||||
'product_id': kit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
Command.create({
|
||||
'product_id': kit_component_1.id,
|
||||
'product_qty': 1,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': kit_component_2.id,
|
||||
'product_qty': 1,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
delivery_form = Form(self.env['stock.picking'])
|
||||
delivery_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
move.product_id = bom_kit.product_id
|
||||
move.product_uom_qty = 4
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
move.product_id = not_kit_1
|
||||
move.product_uom_qty = 4
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
move.product_id = not_kit_2
|
||||
move.product_uom_qty = 3
|
||||
delivery = delivery_form.save()
|
||||
delivery.action_confirm()
|
||||
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_1).qty_done = 3
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_2).qty_done = 3
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_1).qty_done = 4
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_2).qty_done = 2
|
||||
backorder_wizard_dict = delivery.button_validate()
|
||||
backorder_wizard_form = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context']))
|
||||
backorder_wizard_form.save().process_cancel_backorder()
|
||||
|
||||
aggregate_not_kit_values = delivery.move_line_ids._get_aggregated_product_quantities()
|
||||
self.assertEqual(len(aggregate_not_kit_values.keys()), 2)
|
||||
self.assertTrue(all('Not' in val for val in aggregate_not_kit_values), 'Only non kit products should be included')
|
||||
|
||||
aggregate_kit_values = delivery.move_line_ids._get_aggregated_product_quantities(kit_name=bom_kit.product_id.name)
|
||||
self.assertEqual(len(aggregate_kit_values.keys()), 2)
|
||||
self.assertTrue(all('Component' in val for val in aggregate_kit_values), 'Only kit products should be included')
|
||||
258
odoo-bringout-oca-ocb-mrp/mrp/tests/test_stock_report.py
Normal file
258
odoo-bringout-oca-ocb-mrp/mrp/tests/test_stock_report.py
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import Form
|
||||
from odoo.addons.stock.tests.test_report import TestReportsCommon
|
||||
|
||||
|
||||
class TestMrpStockReports(TestReportsCommon):
|
||||
def test_report_forecast_1_mo_count(self):
|
||||
""" Creates and configures a product who could be produce and could be a component.
|
||||
Plans some producing and consumming MO and check the report values.
|
||||
"""
|
||||
# Create a variant attribute.
|
||||
product_chocolate = self.env['product.product'].create({
|
||||
'name': 'Chocolate',
|
||||
'type': 'consu',
|
||||
})
|
||||
product_chococake = self.env['product.product'].create({
|
||||
'name': 'Choco Cake',
|
||||
'type': 'product',
|
||||
})
|
||||
product_double_chococake = self.env['product.product'].create({
|
||||
'name': 'Double Choco Cake',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
# Creates two BOM: one creating a regular slime, one using regular slimes.
|
||||
bom_chococake = self.env['mrp.bom'].create({
|
||||
'product_id': product_chococake.id,
|
||||
'product_tmpl_id': product_chococake.product_tmpl_id.id,
|
||||
'product_uom_id': product_chococake.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_chocolate.id, 'product_qty': 4}),
|
||||
],
|
||||
})
|
||||
bom_double_chococake = self.env['mrp.bom'].create({
|
||||
'product_id': product_double_chococake.id,
|
||||
'product_tmpl_id': product_double_chococake.product_tmpl_id.id,
|
||||
'product_uom_id': product_double_chococake.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_chococake.id, 'product_qty': 2}),
|
||||
],
|
||||
})
|
||||
|
||||
# Creates two MO: one for each BOM.
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_chococake
|
||||
mo_form.bom_id = bom_chococake
|
||||
mo_form.product_qty = 10
|
||||
mo_1 = mo_form.save()
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_double_chococake
|
||||
mo_form.bom_id = bom_double_chococake
|
||||
mo_form.product_qty = 2
|
||||
mo_2 = mo_form.save()
|
||||
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids)
|
||||
draft_picking_qty = docs['draft_picking_qty']
|
||||
draft_production_qty = docs['draft_production_qty']
|
||||
self.assertEqual(len(lines), 0, "Must have 0 line.")
|
||||
self.assertEqual(draft_picking_qty['in'], 0)
|
||||
self.assertEqual(draft_picking_qty['out'], 0)
|
||||
self.assertEqual(draft_production_qty['in'], 10)
|
||||
self.assertEqual(draft_production_qty['out'], 4)
|
||||
|
||||
# Confirms the MO and checks the report lines.
|
||||
mo_1.action_confirm()
|
||||
mo_2.action_confirm()
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids)
|
||||
draft_picking_qty = docs['draft_picking_qty']
|
||||
draft_production_qty = docs['draft_production_qty']
|
||||
self.assertEqual(len(lines), 2, "Must have two line.")
|
||||
line_1 = lines[0]
|
||||
line_2 = lines[1]
|
||||
self.assertEqual(line_1['document_in'].id, mo_1.id)
|
||||
self.assertEqual(line_1['quantity'], 4)
|
||||
self.assertEqual(line_1['document_out'].id, mo_2.id)
|
||||
self.assertEqual(line_2['document_in'].id, mo_1.id)
|
||||
self.assertEqual(line_2['quantity'], 6)
|
||||
self.assertEqual(line_2['document_out'], False)
|
||||
self.assertEqual(draft_picking_qty['in'], 0)
|
||||
self.assertEqual(draft_picking_qty['out'], 0)
|
||||
self.assertEqual(draft_production_qty['in'], 0)
|
||||
self.assertEqual(draft_production_qty['out'], 0)
|
||||
|
||||
def test_report_forecast_2_production_backorder(self):
|
||||
""" Creates a manufacturing order and produces half the quantity.
|
||||
Then creates a backorder and checks the report.
|
||||
"""
|
||||
# Configures the warehouse.
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
# Configures a product.
|
||||
product_apple_pie = self.env['product.product'].create({
|
||||
'name': 'Apple Pie',
|
||||
'type': 'product',
|
||||
})
|
||||
product_apple = self.env['product.product'].create({
|
||||
'name': 'Apple',
|
||||
'type': 'consu',
|
||||
})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': product_apple_pie.id,
|
||||
'product_tmpl_id': product_apple_pie.product_tmpl_id.id,
|
||||
'product_uom_id': product_apple_pie.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_apple.id, 'product_qty': 5}),
|
||||
],
|
||||
})
|
||||
# Creates a MO and validates the pick components.
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_apple_pie
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 4
|
||||
mo_1 = mo_form.save()
|
||||
mo_1.action_confirm()
|
||||
pick = mo_1.move_raw_ids.move_orig_ids.picking_id
|
||||
pick.picking_type_id.show_operations = True # Could be false without demo data, as the lot group is disabled
|
||||
pick_form = Form(pick)
|
||||
with pick_form.move_line_ids_without_package.edit(0) as move_line:
|
||||
move_line.qty_done = 20
|
||||
pick = pick_form.save()
|
||||
pick.button_validate()
|
||||
# Produces 3 products then creates a backorder for the remaining product.
|
||||
mo_form = Form(mo_1)
|
||||
mo_form.qty_producing = 3
|
||||
mo_1 = mo_form.save()
|
||||
action = mo_1.button_mark_done()
|
||||
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder = backorder_form.save()
|
||||
backorder.action_backorder()
|
||||
|
||||
mo_2 = (mo_1.procurement_group_id.mrp_production_ids - mo_1)
|
||||
# Checks the forecast report.
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
|
||||
self.assertEqual(len(lines), 1, "Must have only one line about the backorder")
|
||||
self.assertEqual(lines[0]['document_in'].id, mo_2.id)
|
||||
self.assertEqual(lines[0]['quantity'], 1)
|
||||
self.assertEqual(lines[0]['document_out'], False)
|
||||
|
||||
# Produces the last unit.
|
||||
mo_form = Form(mo_2)
|
||||
mo_form.qty_producing = 1
|
||||
mo_2 = mo_form.save()
|
||||
mo_2.button_mark_done()
|
||||
# Checks the forecast report.
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
|
||||
self.assertEqual(len(lines), 0, "Must have no line")
|
||||
|
||||
def test_report_forecast_3_report_line_corresponding_to_mo_highlighted(self):
|
||||
""" When accessing the report from a MO, checks if the correct MO is highlighted in the report
|
||||
"""
|
||||
product_banana = self.env['product.product'].create({
|
||||
'name': 'Banana',
|
||||
'type': 'product',
|
||||
})
|
||||
product_chocolate = self.env['product.product'].create({
|
||||
'name': 'Chocolate',
|
||||
'type': 'consu',
|
||||
})
|
||||
|
||||
# We create 2 identical MO
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_banana
|
||||
mo_form.product_qty = 10
|
||||
with mo_form.move_raw_ids.new() as move:
|
||||
move.product_id = product_chocolate
|
||||
|
||||
mo_1 = mo_form.save()
|
||||
mo_2 = mo_1.copy()
|
||||
(mo_1 | mo_2).action_confirm()
|
||||
|
||||
# Check for both MO if the highlight (is_matched) corresponds to the correct MO
|
||||
for mo in [mo_1, mo_2]:
|
||||
context = mo.action_product_forecast_report()['context']
|
||||
_, _, lines = self.get_report_forecast(product_template_ids=product_banana.product_tmpl_id.ids, context=context)
|
||||
for line in lines:
|
||||
if line['document_in'] == mo:
|
||||
self.assertTrue(line['is_matched'], "The corresponding MO line should be matched in the forecast report.")
|
||||
else:
|
||||
self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the MO shoud not be matched.")
|
||||
|
||||
def test_subkit_in_delivery_slip(self):
|
||||
"""
|
||||
Suppose this structure:
|
||||
Super Kit --|- Compo 01 x1
|
||||
|- Sub Kit x1 --|- Compo 02 x1
|
||||
| |- Compo 03 x1
|
||||
|
||||
This test ensures that, when delivering one Super Kit, one Sub Kit, one Compo 01 and one Compo 02,
|
||||
and when putting in pack the third component of the Super Kit, the delivery report is correct.
|
||||
"""
|
||||
compo01, compo02, compo03, subkit, superkit = self.env['product.product'].create([{
|
||||
'name': n,
|
||||
'type': 'consu',
|
||||
} for n in ['Compo 01', 'Compo 02', 'Compo 03', 'Sub Kit', 'Super Kit']])
|
||||
|
||||
self.env['mrp.bom'].create([{
|
||||
'product_tmpl_id': subkit.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': compo02.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': compo03.id, 'product_qty': 1}),
|
||||
],
|
||||
}, {
|
||||
'product_tmpl_id': superkit.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': compo01.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': subkit.id, 'product_qty': 1}),
|
||||
],
|
||||
}])
|
||||
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.picking_type_out
|
||||
picking_form.partner_id = self.partner
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = superkit
|
||||
move.product_uom_qty = 1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = subkit
|
||||
move.product_uom_qty = 1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = compo01
|
||||
move.product_uom_qty = 1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = compo02
|
||||
move.product_uom_qty = 1
|
||||
picking = picking_form.save()
|
||||
picking.action_confirm()
|
||||
|
||||
picking.move_ids.quantity_done = 1
|
||||
move = picking.move_ids.filtered(lambda m: m.name == "Super Kit" and m.product_id == compo03)
|
||||
move.move_line_ids.result_package_id = self.env['stock.quant.package'].create({'name': 'Package0001'})
|
||||
picking.button_validate()
|
||||
|
||||
html_report = self.env['ir.actions.report']._render_qweb_html(
|
||||
'stock.report_deliveryslip', picking.ids)[0].decode('utf-8').split('\n')
|
||||
keys = [
|
||||
"Package0001", "Compo 03",
|
||||
"Products with no package assigned", "Compo 01", "Compo 02",
|
||||
"Super Kit", "Compo 01",
|
||||
"Sub Kit", "Compo 02", "Compo 03",
|
||||
]
|
||||
for line in html_report:
|
||||
if not keys:
|
||||
break
|
||||
if keys[0] in line:
|
||||
keys = keys[1:]
|
||||
self.assertFalse(keys, "All keys should be in the report with the defined order")
|
||||
848
odoo-bringout-oca-ocb-mrp/mrp/tests/test_traceability.py
Normal file
848
odoo-bringout-oca-ocb-mrp/mrp/tests/test_traceability.py
Normal file
|
|
@ -0,0 +1,848 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
from freezegun import freeze_time
|
||||
from datetime import datetime
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestTraceability(TestMrpCommon):
|
||||
TRACKING_TYPES = ['none', 'serial', 'lot']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
|
||||
|
||||
def _create_product(self, tracking):
|
||||
return self.env['product.product'].create({
|
||||
'name': 'Product %s' % tracking,
|
||||
'type': 'product',
|
||||
'tracking': tracking,
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
def test_tracking_types_on_mo(self):
|
||||
finished_no_track = self._create_product('none')
|
||||
finished_lot = self._create_product('lot')
|
||||
finished_serial = self._create_product('serial')
|
||||
consumed_no_track = self._create_product('none')
|
||||
consumed_lot = self._create_product('lot')
|
||||
consumed_serial = self._create_product('serial')
|
||||
stock_id = self.env.ref('stock.stock_location_stock').id
|
||||
Lot = self.env['stock.lot']
|
||||
# create inventory
|
||||
quants = self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'product_id': consumed_no_track.id,
|
||||
'inventory_quantity': 3
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'product_id': consumed_lot.id,
|
||||
'inventory_quantity': 3,
|
||||
'lot_id': Lot.create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'product_id': consumed_serial.id,
|
||||
'inventory_quantity': 1,
|
||||
'lot_id': Lot.create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'product_id': consumed_serial.id,
|
||||
'inventory_quantity': 1,
|
||||
'lot_id': Lot.create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'product_id': consumed_serial.id,
|
||||
'inventory_quantity': 1,
|
||||
'lot_id': Lot.create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
|
||||
})
|
||||
quants.action_apply_inventory()
|
||||
|
||||
for finished_product in [finished_no_track, finished_lot, finished_serial]:
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': finished_product.id,
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': consumed_no_track.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': consumed_lot.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': consumed_serial.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = finished_product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_uom_id = self.env.ref('uom.product_uom_unit')
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
|
||||
# Start MO production
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
if finished_product.tracking != 'none':
|
||||
mo_form.lot_producing_id = self.env['stock.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id})
|
||||
mo = mo_form.save()
|
||||
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[2], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
# Check results of traceability
|
||||
context = ({
|
||||
'active_id': mo.id,
|
||||
'model': 'mrp.production',
|
||||
})
|
||||
lines = self.env['stock.traceability.report'].with_context(context).get_lines()
|
||||
self.assertEqual(len(lines), 1, "Should always return 1 line : the final product")
|
||||
final_product = lines[0]
|
||||
self.assertEqual(final_product['unfoldable'], True, "Final product should always be unfoldable")
|
||||
|
||||
# Find parts of the final products
|
||||
lines = self.env['stock.traceability.report'].get_lines(final_product['id'], **{
|
||||
'level': final_product['level'],
|
||||
'model_id': final_product['model_id'],
|
||||
'model_name': final_product['model'],
|
||||
})
|
||||
self.assertEqual(len(lines), 3, "There should be 3 lines. 1 for untracked, 1 for lot, and 1 for serial")
|
||||
|
||||
for line in lines:
|
||||
tracking = line['columns'][1].split(' ')[1]
|
||||
self.assertEqual(
|
||||
line['columns'][-1], "1.00 Units", 'Part with tracking type "%s", should have quantity = 1' % (tracking)
|
||||
)
|
||||
unfoldable = False if tracking == 'none' else True
|
||||
self.assertEqual(
|
||||
line['unfoldable'],
|
||||
unfoldable,
|
||||
'Parts with tracking type "%s", should have be unfoldable : %s' % (tracking, unfoldable)
|
||||
)
|
||||
|
||||
def test_tracking_on_byproducts(self):
|
||||
product_final = self.env['product.product'].create({
|
||||
'name': 'Finished Product',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'Raw 1',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
product_2 = self.env['product.product'].create({
|
||||
'name': 'Raw 2',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
byproduct_1 = self.env['product.product'].create({
|
||||
'name': 'Byproduct 1',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
byproduct_2 = self.env['product.product'].create({
|
||||
'name': 'Byproduct 2',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
bom_1 = self.env['mrp.bom'].create({
|
||||
'product_id': product_final.id,
|
||||
'product_tmpl_id': product_final.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_1.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': product_2.id, 'product_qty': 1})
|
||||
],
|
||||
'byproduct_ids': [
|
||||
(0, 0, {'product_id': byproduct_1.id, 'product_qty': 1, 'product_uom_id': byproduct_1.uom_id.id}),
|
||||
(0, 0, {'product_id': byproduct_2.id, 'product_qty': 1, 'product_uom_id': byproduct_2.uom_id.id})
|
||||
]})
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_final
|
||||
mo_form.bom_id = bom_1
|
||||
mo_form.product_qty = 2
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.lot_producing_id = self.env['stock.lot'].create({
|
||||
'product_id': product_final.id,
|
||||
'name': 'Final_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo = mo_form.save()
|
||||
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': product_1.id,
|
||||
'name': 'Raw_1_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': product_2.id,
|
||||
'name': 'Raw_2_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': byproduct_1.id,
|
||||
'name': 'Byproduct_1_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': byproduct_2.id,
|
||||
'name': 'Byproduct_2_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
mo_form = Form(mo_backorder)
|
||||
mo_form.lot_producing_id = self.env['stock.lot'].create({
|
||||
'product_id': product_final.id,
|
||||
'name': 'Final_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo_form.qty_producing = 1
|
||||
mo_backorder = mo_form.save()
|
||||
|
||||
details_operation_form = Form(
|
||||
mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_1),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': product_1.id,
|
||||
'name': 'Raw_1_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_2),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': product_2.id,
|
||||
'name': 'Raw_2_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': byproduct_1.id,
|
||||
'name': 'Byproduct_1_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': byproduct_2.id,
|
||||
'name': 'Byproduct_2_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
|
||||
mo_backorder.button_mark_done()
|
||||
|
||||
# self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4)
|
||||
# self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6)
|
||||
|
||||
mo = mo | mo_backorder
|
||||
raw_move_lines = mo.move_raw_ids.mapped('move_line_ids')
|
||||
raw_line_raw_1_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_1')
|
||||
self.assertEqual(set(raw_line_raw_1_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
|
||||
raw_line_raw_2_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_1')
|
||||
self.assertEqual(set(raw_line_raw_2_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
|
||||
|
||||
finished_move_lines = mo.move_finished_ids.mapped('move_line_ids')
|
||||
finished_move_line_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_1')
|
||||
self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
finished_move_line_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_2')
|
||||
raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2')
|
||||
raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2')
|
||||
self.assertEqual(finished_move_line_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
|
||||
|
||||
byproduct_move_line_1_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_1')
|
||||
self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
byproduct_move_line_1_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_2')
|
||||
self.assertEqual(byproduct_move_line_1_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
|
||||
|
||||
byproduct_move_line_2_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_1')
|
||||
self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
byproduct_move_line_2_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_2')
|
||||
self.assertEqual(byproduct_move_line_2_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
|
||||
|
||||
def test_reuse_unbuilt_usn(self):
|
||||
"""
|
||||
Produce a SN product
|
||||
Unbuilt it
|
||||
Produce a new SN product with same lot
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='serial')
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.env['stock.quant']._update_available_quantity(p1, stock_location, 1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, stock_location, 1)
|
||||
mo.action_assign()
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo_form.lot_producing_id = lot
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = bom
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
with self.assertLogs(level="WARNING") as log_catcher:
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo_form.lot_producing_id = lot
|
||||
mo = mo_form.save()
|
||||
_logger.warning('Dummy')
|
||||
self.assertEqual(len(log_catcher.output), 1, "Useless warnings: \n%s" % "\n".join(log_catcher.output[:-1]))
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
||||
def test_tracked_and_manufactured_component(self):
|
||||
"""
|
||||
Suppose this structure:
|
||||
productA --|- 1 x productB --|- 1 x productC
|
||||
with productB tracked by lot
|
||||
Ensure that, when we already have some qty of productB (with different lots),
|
||||
the user can produce several productA and can then produce some productB again
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
|
||||
picking_type.use_auto_consume_components_lots = True
|
||||
|
||||
productA, productB, productC = self.env['product.product'].create([{
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
}, {
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
}, {
|
||||
'name': 'Product C',
|
||||
'type': 'consu',
|
||||
}])
|
||||
|
||||
lot_B01, lot_B02, lot_B03 = self.env['stock.lot'].create([{
|
||||
'name': 'lot %s' % i,
|
||||
'product_id': productB.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for i in [1, 2, 3]])
|
||||
|
||||
self.env['mrp.bom'].create([{
|
||||
'product_id': finished.id,
|
||||
'product_tmpl_id': finished.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})],
|
||||
} for finished, component in [(productA, productB), (productB, productC)]])
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(productB, stock_location, 10, lot_id=lot_B01)
|
||||
self.env['stock.quant']._update_available_quantity(productB, stock_location, 5, lot_id=lot_B02)
|
||||
|
||||
# Produce 15 x productA
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = productA
|
||||
mo_form.product_qty = 15
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
action = mo.button_mark_done()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
# Produce 15 x productB
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = productB
|
||||
mo_form.product_qty = 15
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 15
|
||||
mo_form.lot_producing_id = lot_B03
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
self.assertEqual(lot_B01.product_qty, 0)
|
||||
self.assertEqual(lot_B02.product_qty, 0)
|
||||
self.assertEqual(lot_B03.product_qty, 15)
|
||||
self.assertEqual(productA.qty_available, 15)
|
||||
|
||||
def test_last_delivery_traceability(self):
|
||||
"""
|
||||
Suppose this structure (-> means 'produces')
|
||||
1 x Subcomponent A -> 1 x Component A -> 1 x EndProduct A
|
||||
All three tracked by lots. Ensure that after validating Picking A (out)
|
||||
for EndProduct A, all three lots' delivery_ids are set to
|
||||
Picking A.
|
||||
"""
|
||||
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
customer_location = self.env.ref('stock.stock_location_customers')
|
||||
|
||||
# Create the three lot-tracked products.
|
||||
subcomponentA = self._create_product('lot')
|
||||
componentA = self._create_product('lot')
|
||||
endproductA = self._create_product('lot')
|
||||
|
||||
# Create production lots.
|
||||
lot_subcomponentA, lot_componentA, lot_endProductA = self.env['stock.lot'].create([{
|
||||
'name': 'lot %s' % product,
|
||||
'product_id': product.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for product in (subcomponentA, componentA, endproductA)])
|
||||
|
||||
# Create two boms, one for Component A and one for EndProduct A
|
||||
self.env['mrp.bom'].create([{
|
||||
'product_id': finished.id,
|
||||
'product_tmpl_id': finished.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})],
|
||||
} for finished, component in [(endproductA, componentA), (componentA, subcomponentA)]])
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(subcomponentA, stock_location, 1, lot_id=lot_subcomponentA)
|
||||
|
||||
# Produce 1 component A
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = componentA
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.lot_producing_id = lot_componentA
|
||||
mo = mo_form.save()
|
||||
mo.move_raw_ids[0].quantity_done = 1.0
|
||||
mo.button_mark_done()
|
||||
|
||||
# Produce 1 endProduct A
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = endproductA
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.lot_producing_id = lot_endProductA
|
||||
mo = mo_form.save()
|
||||
mo.move_raw_ids[0].quantity_done = 1.0
|
||||
mo.button_mark_done()
|
||||
|
||||
# Create out picking for EndProduct A
|
||||
pickingA_out = self.env['stock.picking'].create({
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': stock_location.id,
|
||||
'location_dest_id': customer_location.id})
|
||||
|
||||
moveA = self.env['stock.move'].create({
|
||||
'name': 'Picking A move',
|
||||
'product_id': endproductA.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': endproductA.uom_id.id,
|
||||
'picking_id': pickingA_out.id,
|
||||
'location_id': stock_location.id,
|
||||
'location_dest_id': customer_location.id})
|
||||
|
||||
# Confirm and assign pickingA
|
||||
pickingA_out.action_confirm()
|
||||
pickingA_out.action_assign()
|
||||
|
||||
# Set move_line lot_id to the mrp.production lot_producing_id
|
||||
moveA.move_line_ids[0].write({
|
||||
'qty_done': 1.0,
|
||||
'lot_id': lot_endProductA.id,
|
||||
})
|
||||
# Transfer picking
|
||||
pickingA_out._action_done()
|
||||
|
||||
# Use concat so that delivery_ids is computed in batch.
|
||||
for lot in lot_subcomponentA.concat(lot_componentA, lot_endProductA):
|
||||
self.assertEqual(lot.delivery_ids.ids, pickingA_out.ids)
|
||||
|
||||
def test_unbuild_scrap_and_unscrap_tracked_component(self):
|
||||
"""
|
||||
Suppose a tracked-by-SN component C. There is one C in stock with SN01.
|
||||
Build a product P that uses C with SN, unbuild P, scrap SN, unscrap SN
|
||||
and rebuild a product with SN in the components
|
||||
"""
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
stock_location = warehouse.lot_stock_id
|
||||
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
serial_number = self.env['stock.lot'].create({
|
||||
'product_id': component.id,
|
||||
'name': 'Super Serial',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=serial_number)
|
||||
|
||||
# produce 1
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_4
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, serial_number)
|
||||
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 1
|
||||
mo.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo.button_mark_done()
|
||||
|
||||
# unbuild
|
||||
action = mo.button_unbuild()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.action_validate()
|
||||
|
||||
# scrap the component
|
||||
scrap = self.env['stock.scrap'].create({
|
||||
'product_id': component.id,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
'scrap_qty': 1,
|
||||
'lot_id': serial_number.id,
|
||||
})
|
||||
scrap_location = scrap.scrap_location_id
|
||||
scrap.do_scrap()
|
||||
|
||||
# unscrap the component
|
||||
internal_move = self.env['stock.move'].create({
|
||||
'name': component.name,
|
||||
'location_id': scrap_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'product_id': component.id,
|
||||
'product_uom': component.uom_id.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'move_line_ids': [(0, 0, {
|
||||
'product_id': component.id,
|
||||
'location_id': scrap_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
'qty_done': 1.0,
|
||||
'lot_id': serial_number.id,
|
||||
})],
|
||||
})
|
||||
internal_move._action_confirm()
|
||||
internal_move._action_done()
|
||||
|
||||
# produce one with the unscrapped component
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_4
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, serial_number)
|
||||
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 1
|
||||
mo.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo.button_mark_done()
|
||||
|
||||
self.assertRecordValues((mo.move_finished_ids + mo.move_raw_ids).move_line_ids, [
|
||||
{'product_id': self.bom_4.product_id.id, 'lot_id': False, 'qty_done': 1},
|
||||
{'product_id': component.id, 'lot_id': serial_number.id, 'qty_done': 1},
|
||||
])
|
||||
|
||||
def test_generate_serial_button(self):
|
||||
"""Test if lot in form "00000dd" is manually created, the generate serial
|
||||
button can skip it and create the next one.
|
||||
"""
|
||||
mo, _bom, p_final, _p1, _p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='lot')
|
||||
|
||||
# generate lot lot_0 on MO
|
||||
mo.action_generate_serial()
|
||||
lot_0 = mo.lot_producing_id.name
|
||||
# manually create lot_1 (lot_0 + 1)
|
||||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': str(int(lot_0) + 1).zfill(7),
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
}).name
|
||||
# generate lot lot_2 on a new MO
|
||||
mo = mo.copy()
|
||||
mo.action_confirm()
|
||||
mo.action_generate_serial()
|
||||
lot_2 = mo.lot_producing_id.name
|
||||
self.assertEqual(lot_2, str(int(lot_1) + 1).zfill(7))
|
||||
|
||||
def test_assign_stock_move_date_on_mark_done(self):
|
||||
product_final = self.env['product.product'].create({
|
||||
'name': 'Finished Product',
|
||||
'type': 'product',
|
||||
})
|
||||
with freeze_time('2024-01-15'):
|
||||
production = self.env['mrp.production'].create({
|
||||
'product_id': product_final.id,
|
||||
'product_qty': 1,
|
||||
'date_planned_start': datetime(2024, 1, 10)
|
||||
})
|
||||
production.action_confirm()
|
||||
production.qty_producing = 1
|
||||
production.button_mark_done()
|
||||
self.assertEqual(production.move_finished_ids.date, datetime(2024, 1, 15), "Stock move should be availbale after the production is done.")
|
||||
|
||||
def test_use_lot_already_consumed(self):
|
||||
"""
|
||||
Tracked-by-sn product
|
||||
Produce SN
|
||||
Consume SN
|
||||
Consume SN -> Should raise an error as it has already been consumed
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
sn_lot01, sn_lot02 = self.env['stock.lot'].create([{
|
||||
'product_id': component.id,
|
||||
'name': name,
|
||||
'company_id': self.env.company.id,
|
||||
} for name in ['SN01', 'SN02']])
|
||||
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=sn_lot02)
|
||||
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1
|
||||
mo.lot_producing_id = sn_lot01
|
||||
mo.button_mark_done()
|
||||
self.assertRecordValues(mo.move_finished_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn_lot01.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_4
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1
|
||||
mo.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo.move_raw_ids.move_line_ids.lot_id = sn_lot01
|
||||
mo.button_mark_done()
|
||||
self.assertRecordValues(mo.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn_lot01.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_4
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1
|
||||
mo.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo.move_raw_ids.move_line_ids.lot_id = sn_lot01
|
||||
with self.assertRaises(UserError):
|
||||
mo.button_mark_done()
|
||||
|
||||
def test_produce_consume_unbuild_and_consume(self):
|
||||
"""
|
||||
(1) Produce SN
|
||||
(2) Consume SN
|
||||
Unbuild (2)
|
||||
Consume SN
|
||||
-> We should not raise any UserError
|
||||
"""
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
sn = self.env['stock.lot'].create({
|
||||
'product_id': component.id,
|
||||
'name': "SN",
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
mo_produce_sn = self.env['mrp.production'].create({
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo_produce_sn.action_confirm()
|
||||
mo_produce_sn.qty_producing = 1
|
||||
mo_produce_sn.lot_producing_id = sn
|
||||
mo_produce_sn.button_mark_done()
|
||||
|
||||
mo_consume_sn_form = Form(self.env['mrp.production'])
|
||||
mo_consume_sn_form.bom_id = self.bom_4
|
||||
mo_consume_sn = mo_consume_sn_form.save()
|
||||
mo_consume_sn.action_confirm()
|
||||
mo_consume_sn.qty_producing = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
|
||||
mo_consume_sn.button_mark_done()
|
||||
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo_consume_sn
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
mo_consume_sn_form = Form(self.env['mrp.production'])
|
||||
mo_consume_sn_form.bom_id = self.bom_4
|
||||
mo_consume_sn = mo_consume_sn_form.save()
|
||||
mo_consume_sn.action_confirm()
|
||||
mo_consume_sn.qty_producing = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
|
||||
mo_consume_sn.button_mark_done()
|
||||
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
def test_produce_consume_unbuild_all_and_consume(self):
|
||||
"""
|
||||
(1) Produce SN
|
||||
(2) Consume SN
|
||||
Unbuild (2)
|
||||
Unbuild (1)
|
||||
Update stock with 1 SN
|
||||
Consume SN
|
||||
-> We should not raise any UserError
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
sn = self.env['stock.lot'].create({
|
||||
'product_id': component.id,
|
||||
'name': "SN",
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
mo_produce_sn = self.env['mrp.production'].create({
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo_produce_sn.action_confirm()
|
||||
mo_produce_sn.qty_producing = 1
|
||||
mo_produce_sn.lot_producing_id = sn
|
||||
mo_produce_sn.button_mark_done()
|
||||
|
||||
mo_consume_sn_form = Form(self.env['mrp.production'])
|
||||
mo_consume_sn_form.bom_id = self.bom_4
|
||||
mo_consume_sn = mo_consume_sn_form.save()
|
||||
mo_consume_sn.action_confirm()
|
||||
mo_consume_sn.qty_producing = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
|
||||
mo_consume_sn.button_mark_done()
|
||||
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo_consume_sn
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo_produce_sn
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=sn)
|
||||
|
||||
mo_consume_sn_form = Form(self.env['mrp.production'])
|
||||
mo_consume_sn_form.bom_id = self.bom_4
|
||||
mo_consume_sn = mo_consume_sn_form.save()
|
||||
mo_consume_sn.action_confirm()
|
||||
mo_consume_sn.qty_producing = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
|
||||
mo_consume_sn.button_mark_done()
|
||||
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
])
|
||||
982
odoo-bringout-oca-ocb-mrp/mrp/tests/test_unbuild.py
Normal file
982
odoo-bringout-oca-ocb-mrp/mrp/tests/test_unbuild.py
Normal file
|
|
@ -0,0 +1,982 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestUnbuild(TestMrpCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
cls.env.ref('base.group_user').write({
|
||||
'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]
|
||||
})
|
||||
|
||||
def test_unbuild_standart(self):
|
||||
""" This test creates a MO and then creates 3 unbuild
|
||||
orders for the final product. None of the products for this
|
||||
test are tracked. It checks the stock state after each order
|
||||
and ensure it is correct.
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo()
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
# Check quantity in stock before unbuild.
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
# ---------------------------------------------------
|
||||
# unbuild
|
||||
# ---------------------------------------------------
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 3
|
||||
x.save().action_unbuild()
|
||||
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 2
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 5
|
||||
x.save().action_unbuild()
|
||||
|
||||
# Check quantity in stock after last unbuild.
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
def test_unbuild_with_final_lot(self):
|
||||
""" This test creates a MO and then creates 3 unbuild
|
||||
orders for the final product. Only the final product is tracked
|
||||
by lot. It checks the stock state after each order
|
||||
and ensure it is correct.
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot')
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo_form.lot_producing_id = lot
|
||||
mo = mo_form.save()
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
# Check quantity in stock before unbuild.
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 5, 'You should have the 5 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
# ---------------------------------------------------
|
||||
# unbuild
|
||||
# ---------------------------------------------------
|
||||
|
||||
# This should fail since we do not choose a lot to unbuild for final product.
|
||||
with self.assertRaises(AssertionError):
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 3
|
||||
unbuild_order = x.save()
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 3
|
||||
x.lot_id = lot
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 2, 'You should have consumed 3 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 2
|
||||
x.lot_id = lot
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 0, 'You should have 0 finalproduct in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 5
|
||||
x.lot_id = lot
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
def test_unbuild_with_comnsumed_lot(self):
|
||||
""" This test creates a MO and then creates 3 unbuild
|
||||
orders for the final product. Only once of the two consumed
|
||||
product is tracked by lot. It checks the stock state after each
|
||||
order and ensure it is correct.
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot')
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
mo.action_assign()
|
||||
for ml in mo.move_raw_ids.mapped('move_line_ids'):
|
||||
if ml.product_id.tracking != 'none':
|
||||
self.assertEqual(ml.lot_id, lot, 'Wrong reserved lot.')
|
||||
|
||||
# FIXME sle: behavior change
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = lot
|
||||
ml.qty_done = 20
|
||||
details_operation_form.save()
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# Check quantity in stock before unbuild.
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 80, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
# ---------------------------------------------------
|
||||
# unbuild
|
||||
# ---------------------------------------------------
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 3
|
||||
unbuild_order = x.save()
|
||||
|
||||
# This should fail since we do not provide the MO that we wanted to unbuild. (without MO we do not know which consumed lot we have to restore)
|
||||
with self.assertRaises(UserError):
|
||||
unbuild_order.action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have consumed 3 final product in stock')
|
||||
|
||||
unbuild_order.mo_id = mo.id
|
||||
unbuild_order.action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 92, 'You should have 92 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.product_qty = 2
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 100, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.product_qty = 5
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 120, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
def test_unbuild_with_everything_tracked(self):
|
||||
""" This test creates a MO and then creates 3 unbuild
|
||||
orders for the final product. All the products for this
|
||||
test are tracked. It checks the stock state after each order
|
||||
and ensure it is correct.
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_2='lot', tracking_base_1='lot')
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
lot_final = self.env['stock.lot'].create({
|
||||
'name': 'lot_final',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_consumed_1',
|
||||
'product_id': p1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_consumed_2',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot_1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2)
|
||||
mo.action_assign()
|
||||
|
||||
# FIXME sle: behavior change
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo_form.lot_producing_id = lot_final
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 5
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 20
|
||||
details_operation_form.save()
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# Check quantity in stock before unbuild.
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have the 5 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 80, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
# ---------------------------------------------------
|
||||
# unbuild
|
||||
# ---------------------------------------------------
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
with self.assertRaises(AssertionError):
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 3
|
||||
x.save()
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 3
|
||||
x.save()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock')
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.product_qty = 3
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 2, 'You should have consumed 3 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 92 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.product_qty = 2
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 0, 'You should have 0 finalproduct in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 100, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 5, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.product_qty = 5
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 120, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 10, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
def test_unbuild_with_duplicate_move(self):
|
||||
""" This test creates a MO from 3 different lot on a consumed product (p2).
|
||||
The unbuild order should revert the correct quantity for each specific lot.
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='none', tracking_base_2='lot', tracking_base_1='none')
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_1',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_2',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_3 = self.env['stock.lot'].create({
|
||||
'name': 'lot_3',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=lot_1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_2)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot_3)
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids.filtered(lambda ml: ml.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = ml.reserved_uom_qty
|
||||
with details_operation_form.move_line_ids.edit(1) as ml:
|
||||
ml.qty_done = ml.reserved_uom_qty
|
||||
with details_operation_form.move_line_ids.edit(2) as ml:
|
||||
ml.qty_done = ml.reserved_uom_qty
|
||||
details_operation_form.save()
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# Check quantity in stock before unbuild.
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 0, 'You should have consumed all the 1 product for lot 1 in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 3 product for lot 2 in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 1, 'You should have consumed only 1 product for lot3 in stock')
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.product_qty = 5
|
||||
x.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have no more final product in stock after unbuild')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 1, 'You should have get your product with lot 1 in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have the 3 basic product for lot 2 in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 2, 'You should have get one product back for lot 3')
|
||||
|
||||
def test_production_links_with_non_tracked_lots(self):
|
||||
""" This test produces an MO in two times and checks that the move lines are linked in a correct way
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_1='none', tracking_base_2='lot')
|
||||
# Young Tom
|
||||
# \ Botox - 4 - p1
|
||||
# \ Old Tom - 1 - p2
|
||||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_1',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_1)
|
||||
lot_finished_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_finished_1',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.assertEqual(mo.product_qty, 5)
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 3.0
|
||||
mo_form.lot_producing_id = lot_finished_1
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(mo.move_raw_ids[1].quantity_done, 12)
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 3
|
||||
ml.lot_id = lot_1
|
||||
details_operation_form.save()
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env[action['res_model']].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
lot_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_2',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 4, lot_id=lot_2)
|
||||
lot_finished_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_finished_2',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
mo = mo.procurement_group_id.mrp_production_ids[1]
|
||||
# FIXME sle: issue in backorder?
|
||||
mo.move_raw_ids.move_line_ids.unlink()
|
||||
self.assertEqual(mo.product_qty, 2)
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 2
|
||||
mo_form.lot_producing_id = lot_finished_2
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 2
|
||||
ml.lot_id = lot_2
|
||||
details_operation_form.save()
|
||||
action = mo.button_mark_done()
|
||||
|
||||
mo1 = mo.procurement_group_id.mrp_production_ids[0]
|
||||
ml = mo1.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_1 in m.produce_line_ids.lot_id)
|
||||
self.assertEqual(sum(ml.mapped('qty_done')), 12.0, 'Should have consumed 12 for the first lot')
|
||||
ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_2 in m.produce_line_ids.lot_id)
|
||||
self.assertEqual(sum(ml.mapped('qty_done')), 8.0, 'Should have consumed 8 for the second lot')
|
||||
|
||||
def test_unbuild_with_routes(self):
|
||||
""" This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock
|
||||
is created with Warehouse -> True.
|
||||
The unbuild order should revert the consumed components into QC/Unbuild location for quality check
|
||||
and then a picking should be generated for transferring components from QC/Unbuild location to stock.
|
||||
"""
|
||||
StockQuant = self.env['stock.quant']
|
||||
ProductObj = self.env['product.product']
|
||||
# Create new QC/Unbuild location
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
unbuild_location = self.env['stock.location'].create({
|
||||
'name': 'QC/Unbuild',
|
||||
'usage': 'internal',
|
||||
'location_id': warehouse.view_location_id.id
|
||||
})
|
||||
|
||||
# Create a product route containing a stock rule that will move product from QC/Unbuild location to stock
|
||||
self.env['stock.route'].create({
|
||||
'name': 'QC/Unbuild -> Stock',
|
||||
'warehouse_selectable': True,
|
||||
'warehouse_ids': [(4, warehouse.id)],
|
||||
'rule_ids': [(0, 0, {
|
||||
'name': 'Send Matrial QC/Unbuild -> Stock',
|
||||
'action': 'push',
|
||||
'picking_type_id': self.ref('stock.picking_type_internal'),
|
||||
'location_src_id': unbuild_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
})],
|
||||
})
|
||||
|
||||
# Create a stockable product and its components
|
||||
finshed_product = ProductObj.create({
|
||||
'name': 'Table',
|
||||
'type': 'product',
|
||||
})
|
||||
component1 = ProductObj.create({
|
||||
'name': 'Table head',
|
||||
'type': 'product',
|
||||
})
|
||||
component2 = ProductObj.create({
|
||||
'name': 'Table stand',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
# Create bom and add components
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': finshed_product.id,
|
||||
'product_tmpl_id': finshed_product.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component1.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': component2.id, 'product_qty': 1})
|
||||
]})
|
||||
|
||||
# Set on hand quantity
|
||||
StockQuant._update_available_quantity(component1, self.stock_location, 1)
|
||||
StockQuant._update_available_quantity(component2, self.stock_location, 1)
|
||||
|
||||
# Create mo
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = finshed_product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_uom_id = finshed_product.uom_id
|
||||
mo_form.product_qty = 1.0
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
|
||||
# Produce the final product
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
produce_wizard = mo_form.save()
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
# Check quantity in stock before unbuild
|
||||
self.assertEqual(StockQuant._get_available_quantity(finshed_product, self.stock_location), 1, 'Table should be available in stock')
|
||||
self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 0, 'Table head should not be available in stock')
|
||||
self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 0, 'Table stand should not be available in stock')
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Unbuild
|
||||
# ---------------------------------------------------
|
||||
|
||||
# Create an unbuild order of the finished product and set the destination loacation = QC/Unbuild
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = finshed_product
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.product_qty = 1
|
||||
x.location_id = self.stock_location
|
||||
x.location_dest_id = unbuild_location
|
||||
x.save().action_unbuild()
|
||||
|
||||
# Check the available quantity of components and final product in stock
|
||||
self.assertEqual(StockQuant._get_available_quantity(finshed_product, self.stock_location), 0, 'Table should not be available in stock as it is unbuild')
|
||||
self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 0, 'Table head should not be available in stock as it is in QC/Unbuild location')
|
||||
self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 0, 'Table stand should not be available in stock as it is in QC/Unbuild location')
|
||||
|
||||
# Find new generated picking
|
||||
picking = self.env['stock.picking'].search([('product_id', 'in', [component1.id, component2.id])])
|
||||
self.assertEqual(picking.location_id.id, unbuild_location.id, 'Wrong source location in picking')
|
||||
self.assertEqual(picking.location_dest_id.id, self.stock_location.id, 'Wrong destination location in picking')
|
||||
|
||||
# Transfer it
|
||||
for ml in picking.move_ids_without_package:
|
||||
ml.quantity_done = 1
|
||||
picking._action_done()
|
||||
|
||||
# Check the available quantity of components and final product in stock
|
||||
self.assertEqual(StockQuant._get_available_quantity(finshed_product, self.stock_location), 0, 'Table should not be available in stock')
|
||||
self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 1, 'Table head should be available in stock as the picking is transferred')
|
||||
self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 1, 'Table stand should be available in stock as the picking is transferred')
|
||||
|
||||
def test_unbuild_decimal_qty(self):
|
||||
"""
|
||||
Use case:
|
||||
- decimal accuracy of Product UoM > decimal accuracy of Units
|
||||
- unbuild a product with a decimal quantity of component
|
||||
"""
|
||||
self.env['decimal.precision'].search([('name', '=', 'Product Unit of Measure')]).digits = 4
|
||||
self.uom_unit.rounding = 0.001
|
||||
|
||||
self.bom_1.product_qty = 3
|
||||
self.bom_1.bom_line_ids.product_qty = 5
|
||||
self.env['stock.quant']._update_available_quantity(self.product_2, self.stock_location, 3)
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.bom_1.product_id
|
||||
mo_form.bom_id = self.bom_1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 3
|
||||
mo_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
uo_form = Form(self.env['mrp.unbuild'])
|
||||
uo_form.mo_id = mo
|
||||
# Unbuilding one product means a decimal quantity equal to 1 / 3 * 5 for each component
|
||||
uo_form.product_qty = 1
|
||||
uo = uo_form.save()
|
||||
uo.action_unbuild()
|
||||
self.assertEqual(uo.state, 'done')
|
||||
|
||||
def test_unbuild_similar_tracked_components(self):
|
||||
"""
|
||||
Suppose a MO with, in the components, two lines for the same tracked-by-usn product
|
||||
When unbuilding such an MO, all SN used in the MO should be back in stock
|
||||
"""
|
||||
compo, finished = self.env['product.product'].create([{
|
||||
'name': 'compo',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
}, {
|
||||
'name': 'finished',
|
||||
'type': 'product',
|
||||
}])
|
||||
|
||||
lot01, lot02 = self.env['stock.lot'].create([{
|
||||
'name': n,
|
||||
'product_id': compo.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for n in ['lot01', 'lot02']])
|
||||
self.env['stock.quant']._update_available_quantity(compo, self.stock_location, 1, lot_id=lot01)
|
||||
self.env['stock.quant']._update_available_quantity(compo, self.stock_location, 1, lot_id=lot02)
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = finished
|
||||
with mo_form.move_raw_ids.new() as line:
|
||||
line.product_id = compo
|
||||
line.product_uom_qty = 1
|
||||
with mo_form.move_raw_ids.new() as line:
|
||||
line.product_id = compo
|
||||
line.product_uom_qty = 1
|
||||
mo = mo_form.save()
|
||||
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_assign()
|
||||
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
uo_form = Form(self.env['mrp.unbuild'])
|
||||
uo_form.mo_id = mo
|
||||
uo_form.product_qty = 1
|
||||
uo = uo_form.save()
|
||||
uo.action_unbuild()
|
||||
|
||||
self.assertEqual(uo.produce_line_ids.filtered(lambda sm: sm.product_id == compo).lot_ids, lot01 + lot02)
|
||||
|
||||
def test_unbuild_and_multilocations(self):
|
||||
"""
|
||||
Basic flow: produce p_final, transfer it to a sub-location and then
|
||||
unbuild it. The test ensures that the source/destination locations of an
|
||||
unbuild order are applied on the stock moves
|
||||
"""
|
||||
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
|
||||
self.env.user.write({'groups_id': [(4, grp_multi_loc.id, 0)]})
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||||
prod_location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.env.user.id)])
|
||||
subloc01, subloc02, = self.stock_location.child_ids[:2]
|
||||
|
||||
mo, _, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1)
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
# Transfer the finished product from WH/Stock to `subloc01`
|
||||
internal_form = Form(self.env['stock.picking'])
|
||||
internal_form.picking_type_id = warehouse.int_type_id
|
||||
internal_form.location_id = self.stock_location
|
||||
internal_form.location_dest_id = subloc01
|
||||
with internal_form.move_ids_without_package.new() as move:
|
||||
move.product_id = p_final
|
||||
move.product_uom_qty = 1.0
|
||||
internal_transfer = internal_form.save()
|
||||
internal_transfer.action_confirm()
|
||||
internal_transfer.action_assign()
|
||||
internal_transfer.move_line_ids.qty_done = 1.0
|
||||
internal_transfer.button_validate()
|
||||
|
||||
unbuild_order_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_order_form.mo_id = mo
|
||||
unbuild_order_form.location_id = subloc01
|
||||
unbuild_order_form.location_dest_id = subloc02
|
||||
unbuild_order = unbuild_order_form.save()
|
||||
unbuild_order.action_unbuild()
|
||||
|
||||
self.assertRecordValues(unbuild_order.produce_line_ids, [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'product_id': p_final.id, 'location_id': subloc01.id, 'location_dest_id': prod_location.id},
|
||||
{'product_id': p2.id, 'location_id': prod_location.id, 'location_dest_id': subloc02.id},
|
||||
{'product_id': p1.id, 'location_id': prod_location.id, 'location_dest_id': subloc02.id},
|
||||
])
|
||||
|
||||
def test_compute_product_uom_id(self):
|
||||
order = self.env['mrp.unbuild'].create({
|
||||
'product_id': self.product_4.id,
|
||||
})
|
||||
self.assertEqual(order.product_uom_id, self.product_4.uom_id)
|
||||
|
||||
def test_compute_location_id(self):
|
||||
order = self.env['mrp.unbuild'].create({
|
||||
'product_id': self.product_4.id,
|
||||
})
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
self.assertEqual(order.location_id, warehouse.lot_stock_id)
|
||||
self.assertEqual(order.location_dest_id, warehouse.lot_stock_id)
|
||||
|
||||
def test_use_unbuilt_sn_in_mo(self):
|
||||
"""
|
||||
use an unbuilt serial number in manufacturing order:
|
||||
produce a tracked product, unbuild it and then use it as a component with the same SN in a mo.
|
||||
"""
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'Product tracked by sn',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
product_1_sn = self.env['stock.lot'].create({
|
||||
'name': 'sn1',
|
||||
'product_id': product_1.id,
|
||||
'company_id': self.env.company.id
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Product component',
|
||||
'type': 'product',
|
||||
})
|
||||
bom_1 = self.env['mrp.bom'].create({
|
||||
'product_id': product_1.id,
|
||||
'product_tmpl_id': product_1.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
product_2 = self.env['product.product'].create({
|
||||
'name': 'finished Product',
|
||||
'type': 'product',
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_2.id,
|
||||
'product_tmpl_id': product_2.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_1.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
# mo1
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_1
|
||||
mo_form.bom_id = bom_1
|
||||
mo_form.product_qty = 1.0
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo_form.lot_producing_id = product_1_sn
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
#unbuild order
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
#mo2
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_2
|
||||
mo2 = mo_form.save()
|
||||
mo2.action_confirm()
|
||||
details_operation_form = Form(mo2.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = product_1_sn
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
mo_form = Form(mo2)
|
||||
mo_form.qty_producing = 1
|
||||
mo2 = mo_form.save()
|
||||
mo2.button_mark_done()
|
||||
self.assertEqual(mo2.state, 'done', "Production order should be in done state.")
|
||||
|
||||
def test_unbuild_mo_with_tracked_product_and_component(self):
|
||||
"""
|
||||
Test that the unbuild order is correctly created when the finished product
|
||||
and the component is tracked by serial number
|
||||
"""
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Product tracked by sn',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
finished_product_sn = self.env['stock.lot'].create({
|
||||
'name': 'sn1',
|
||||
'product_id': finished_product.id,
|
||||
'company_id': self.env.company.id
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Product component',
|
||||
'type': 'product',
|
||||
})
|
||||
bom_1 = self.env['mrp.bom'].create({
|
||||
'product_id': finished_product.id,
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
# mo_1
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = finished_product
|
||||
mo_form.bom_id = bom_1
|
||||
mo_form.product_qty = 1.0
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1.0
|
||||
mo.lot_producing_id = finished_product_sn
|
||||
mo.move_raw_ids.quantity_done = 1
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# unbuild order mo_1
|
||||
action = mo.button_unbuild()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.action_validate()
|
||||
self.assertEqual(mo.unbuild_ids.produce_line_ids[0].product_id, finished_product)
|
||||
self.assertEqual(mo.unbuild_ids.produce_line_ids[0].lot_ids, finished_product_sn)
|
||||
self.assertEqual(mo.unbuild_ids.produce_line_ids[1].product_id, component)
|
||||
self.assertEqual(mo.unbuild_ids.produce_line_ids[1].lot_ids.id, False)
|
||||
|
||||
# set the component as tracked
|
||||
component.tracking = 'serial'
|
||||
component_sn = self.env['stock.lot'].create({
|
||||
'name': 'component-sn1',
|
||||
'product_id': component.id,
|
||||
'company_id': self.env.company.id
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=component_sn)
|
||||
#mo2 with tracked component
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = finished_product
|
||||
mo_form.bom_id = bom_1
|
||||
mo_form.product_qty = 1.0
|
||||
mo_2 = mo_form.save()
|
||||
mo_2.action_confirm()
|
||||
mo_2.qty_producing = 1.0
|
||||
mo_2.lot_producing_id = finished_product_sn
|
||||
mo_2.move_raw_ids.quantity_done = 1
|
||||
mo_2.button_mark_done()
|
||||
self.assertEqual(mo_2.state, 'done', "Production order should be in done state.")
|
||||
# unbuild mo_2
|
||||
action = mo_2.button_unbuild()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.action_validate()
|
||||
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[0].product_id, finished_product)
|
||||
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[0].lot_ids, finished_product_sn)
|
||||
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[1].product_id, component)
|
||||
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[1].lot_ids, component_sn)
|
||||
|
||||
def test_unbuild_different_qty(self):
|
||||
"""
|
||||
Test that the quantity to unbuild is the qty produced in the MO
|
||||
|
||||
BoM:
|
||||
- 4x final product
|
||||
components:
|
||||
- 2 x (storable)
|
||||
- 4 x (consumable)
|
||||
- Create a MO with 4 final products to produce.
|
||||
- Confirm and validate, then unlock the mo and update the qty produced to 10
|
||||
- open the wizard to unbuild > the quantity proposed should be 10
|
||||
- unbuild 4 units
|
||||
- the move lines should be created with the correct quantity
|
||||
"""
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_1
|
||||
mo = mo_form.save()
|
||||
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 4
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# unlock and update the qty produced
|
||||
mo.action_toggle_is_locked()
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 10
|
||||
self.assertEqual(mo.qty_producing, 10)
|
||||
#unbuild order
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
# check that the quantity to unbuild is the qty produced in the MO
|
||||
self.assertEqual(unbuild_form.product_qty, 10)
|
||||
unbuild_form.product_qty = 3
|
||||
unbuild_order = unbuild_form.save()
|
||||
unbuild_order.action_unbuild()
|
||||
self.assertRecordValues(unbuild_order.produce_line_ids.move_line_ids, [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'product_id': self.bom_1.product_id.id, 'qty_done': 3},
|
||||
{'product_id': self.bom_1.bom_line_ids[0].product_id.id, 'qty_done': 0.6},
|
||||
{'product_id': self.bom_1.bom_line_ids[1].product_id.id, 'qty_done': 1.2},
|
||||
])
|
||||
|
||||
def test_unbuild_update_forecasted_qty(self):
|
||||
"""
|
||||
Test that the unbuild correctly updates the forecasted quantity of a product.
|
||||
"""
|
||||
bom = self.bom_4
|
||||
product = bom.product_id
|
||||
# qty_available + incoming_qty - outgoing_qty = virtual_available
|
||||
# Currently: 0.0 + 0.0 - 0.0 = 0.0
|
||||
self.assertRecordValues(product, [{"qty_available": 0.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 0.0}])
|
||||
# Manufacture 20 unit
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 20.0
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertRecordValues(product, [{"qty_available": 0.0, "incoming_qty": 20.0, "outgoing_qty": 0.0, "virtual_available": 20.0}])
|
||||
mo.qty_producing = 20.0
|
||||
mo.move_raw_ids.quantity_done = 20.0
|
||||
mo.button_mark_done()
|
||||
self.assertRecordValues(product, [{"qty_available": 20.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 20.0}])
|
||||
# Unlock the MO and add 10 additional produced quantity
|
||||
mo.action_toggle_is_locked()
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 30.0
|
||||
mo = mo_form.save()
|
||||
self.assertRecordValues(product, [{"qty_available": 30.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 30.0}])
|
||||
# Unbuild the 15 units
|
||||
action = mo.button_unbuild()
|
||||
unbuild_form = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
unbuild_form.product_qty = 15.0
|
||||
wizard = unbuild_form.save()
|
||||
wizard.action_validate()
|
||||
self.assertRecordValues(product, [{"qty_available": 15.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 15.0}])
|
||||
|
|
@ -0,0 +1,697 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Required for `uom_id` to be visible in the view
|
||||
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
|
||||
# Required for `manufacture_steps` to be visible in the view
|
||||
cls.env.user.groups_id += cls.env.ref('stock.group_adv_location')
|
||||
# Create warehouse
|
||||
cls.customer_location = cls.env['ir.model.data']._xmlid_to_res_id('stock.stock_location_customers')
|
||||
warehouse_form = Form(cls.env['stock.warehouse'])
|
||||
warehouse_form.name = 'Test Warehouse'
|
||||
warehouse_form.code = 'TWH'
|
||||
cls.warehouse = warehouse_form.save()
|
||||
|
||||
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
|
||||
|
||||
# Create manufactured product
|
||||
product_form = Form(cls.env['product.product'])
|
||||
product_form.name = 'Stick'
|
||||
product_form.uom_id = cls.uom_unit
|
||||
product_form.uom_po_id = cls.uom_unit
|
||||
product_form.detailed_type = 'product'
|
||||
product_form.route_ids.clear()
|
||||
product_form.route_ids.add(cls.warehouse.manufacture_pull_id.route_id)
|
||||
product_form.route_ids.add(cls.warehouse.mto_pull_id.route_id)
|
||||
cls.finished_product = product_form.save()
|
||||
|
||||
# Create raw product for manufactured product
|
||||
product_form = Form(cls.env['product.product'])
|
||||
product_form.name = 'Raw Stick'
|
||||
product_form.detailed_type = 'product'
|
||||
product_form.uom_id = cls.uom_unit
|
||||
product_form.uom_po_id = cls.uom_unit
|
||||
cls.raw_product = product_form.save()
|
||||
|
||||
# Create bom for manufactured product
|
||||
bom_product_form = Form(cls.env['mrp.bom'])
|
||||
bom_product_form.product_id = cls.finished_product
|
||||
bom_product_form.product_tmpl_id = cls.finished_product.product_tmpl_id
|
||||
bom_product_form.product_qty = 1.0
|
||||
bom_product_form.type = 'normal'
|
||||
with bom_product_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = cls.raw_product
|
||||
bom_line.product_qty = 2.0
|
||||
|
||||
cls.bom = bom_product_form.save()
|
||||
|
||||
def _check_location_and_routes(self):
|
||||
# Check manufacturing pull rule.
|
||||
self.assertTrue(self.warehouse.manufacture_pull_id)
|
||||
self.assertTrue(self.warehouse.manufacture_pull_id.active, self.warehouse.manufacture_to_resupply)
|
||||
self.assertTrue(self.warehouse.manufacture_pull_id.route_id)
|
||||
# Check new routes created or not.
|
||||
self.assertTrue(self.warehouse.pbm_route_id)
|
||||
# Check location should be created and linked to warehouse.
|
||||
self.assertTrue(self.warehouse.pbm_loc_id)
|
||||
self.assertEqual(self.warehouse.pbm_loc_id.active, self.warehouse.manufacture_steps != 'mrp_one_step', "Input location must be de-active for single step only.")
|
||||
self.assertTrue(self.warehouse.manu_type_id.active)
|
||||
|
||||
def test_00_create_warehouse(self):
|
||||
""" Warehouse testing for direct manufacturing """
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'mrp_one_step'
|
||||
self._check_location_and_routes()
|
||||
# Check locations of existing pull rule
|
||||
self.assertFalse(self.warehouse.pbm_route_id.rule_ids, 'only the update of global manufacture route should happen.')
|
||||
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.lot_stock_id.id)
|
||||
|
||||
def test_01_warehouse_twostep_manufacturing(self):
|
||||
""" Warehouse testing for picking before manufacturing """
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm'
|
||||
self._check_location_and_routes()
|
||||
self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 2)
|
||||
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.lot_stock_id.id)
|
||||
|
||||
def test_02_warehouse_twostep_manufacturing(self):
|
||||
""" Warehouse testing for picking ans store after manufacturing """
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
self._check_location_and_routes()
|
||||
self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 3)
|
||||
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.sam_loc_id.id)
|
||||
|
||||
def test_manufacturing_3_steps(self):
|
||||
""" Test MO/picking before manufacturing/picking after manufacturing
|
||||
components and move_orig/move_dest. Ensure that everything is created
|
||||
correctly.
|
||||
"""
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.finished_product
|
||||
production_form.picking_type_id = self.warehouse.manu_type_id
|
||||
production = production_form.save()
|
||||
production.action_confirm()
|
||||
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 1)
|
||||
self.assertEqual(move_raw_ids.product_id, self.raw_product)
|
||||
self.assertEqual(move_raw_ids.picking_type_id, self.warehouse.manu_type_id)
|
||||
pbm_move = move_raw_ids.move_orig_ids
|
||||
self.assertEqual(len(pbm_move), 1)
|
||||
self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
|
||||
self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
|
||||
self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
|
||||
self.assertFalse(pbm_move.move_orig_ids)
|
||||
|
||||
move_finished_ids = production.move_finished_ids
|
||||
self.assertEqual(len(move_finished_ids), 1)
|
||||
self.assertEqual(move_finished_ids.product_id, self.finished_product)
|
||||
self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
|
||||
sam_move = move_finished_ids.move_dest_ids
|
||||
self.assertEqual(len(sam_move), 1)
|
||||
self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
|
||||
self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
|
||||
self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
|
||||
self.assertFalse(sam_move.move_dest_ids)
|
||||
|
||||
def test_manufacturing_flow(self):
|
||||
""" Simulate a pick pack ship delivery combined with a picking before
|
||||
manufacturing and store after manufacturing. Also ensure that the MO and
|
||||
the moves to stock are created with the generic pull rules.
|
||||
In order to trigger the rule we create a picking to the customer with
|
||||
the 'make to order' procure method
|
||||
"""
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
warehouse.delivery_steps = 'pick_pack_ship'
|
||||
self.warehouse.flush_model()
|
||||
self.env.ref('stock.route_warehouse0_mto').active = True
|
||||
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
|
||||
picking_customer = self.env['stock.picking'].create({
|
||||
'location_id': self.warehouse.wh_output_stock_loc_id.id,
|
||||
'location_dest_id': self.customer_location,
|
||||
'partner_id': self.env['ir.model.data']._xmlid_to_res_id('base.res_partner_4'),
|
||||
'picking_type_id': self.warehouse.out_type_id.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': self.finished_product.name,
|
||||
'product_id': self.finished_product.id,
|
||||
'product_uom_qty': 2,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'picking_id': picking_customer.id,
|
||||
'location_id': self.warehouse.wh_output_stock_loc_id.id,
|
||||
'location_dest_id': self.customer_location,
|
||||
'procure_method': 'make_to_order',
|
||||
'origin': 'SOURCEDOCUMENT',
|
||||
'state': 'draft',
|
||||
})
|
||||
picking_customer.action_confirm()
|
||||
production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
|
||||
self.assertTrue(production_order)
|
||||
self.assertEqual(production_order.origin, 'SOURCEDOCUMENT', 'The MO origin should be the SO name')
|
||||
self.assertNotEqual(production_order.name, 'SOURCEDOCUMENT', 'The MO name should not be the origin of the move')
|
||||
|
||||
picking_stock_preprod = self.env['stock.move'].search([
|
||||
('product_id', '=', self.raw_product.id),
|
||||
('location_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
|
||||
('picking_type_id', '=', self.warehouse.pbm_type_id.id)
|
||||
]).picking_id
|
||||
picking_stock_postprod = self.env['stock.move'].search([
|
||||
('product_id', '=', self.finished_product.id),
|
||||
('location_id', '=', self.warehouse.sam_loc_id.id),
|
||||
('location_dest_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('picking_type_id', '=', self.warehouse.sam_type_id.id)
|
||||
]).picking_id
|
||||
|
||||
self.assertTrue(picking_stock_preprod)
|
||||
self.assertTrue(picking_stock_postprod)
|
||||
self.assertEqual(picking_stock_preprod.state, 'assigned')
|
||||
self.assertEqual(picking_stock_postprod.state, 'waiting')
|
||||
self.assertEqual(picking_stock_preprod.origin, production_order.name, 'The pre-prod origin should be the MO name')
|
||||
self.assertEqual(picking_stock_postprod.origin, 'SOURCEDOCUMENT', 'The post-prod origin should be the SO name')
|
||||
|
||||
picking_stock_preprod.action_assign()
|
||||
picking_stock_preprod.move_line_ids.qty_done = 4
|
||||
picking_stock_preprod._action_done()
|
||||
|
||||
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.lot_stock_id).mapped('quantity')))
|
||||
self.assertTrue(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id))
|
||||
|
||||
production_order.action_assign()
|
||||
self.assertEqual(production_order.reservation_state, 'assigned')
|
||||
self.assertEqual(picking_stock_postprod.state, 'waiting')
|
||||
|
||||
produce_form = Form(production_order)
|
||||
produce_form.qty_producing = production_order.product_qty
|
||||
production_order = produce_form.save()
|
||||
production_order.button_mark_done()
|
||||
|
||||
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id).mapped('quantity')))
|
||||
|
||||
self.assertEqual(picking_stock_postprod.state, 'assigned')
|
||||
|
||||
picking_stock_pick = self.env['stock.move'].search([
|
||||
('product_id', '=', self.finished_product.id),
|
||||
('location_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('location_dest_id', '=', self.warehouse.wh_pack_stock_loc_id.id),
|
||||
('picking_type_id', '=', self.warehouse.pick_type_id.id)
|
||||
]).picking_id
|
||||
self.assertEqual(picking_stock_pick.move_ids.move_orig_ids.picking_id, picking_stock_postprod)
|
||||
|
||||
def test_cancel_propagation(self):
|
||||
""" Test cancelling moves in a 'picking before
|
||||
manufacturing' and 'store after manufacturing' process. The propagation of
|
||||
cancel depends on the default values on each rule of the chain.
|
||||
"""
|
||||
self.warehouse.manufacture_steps = 'pbm_sam'
|
||||
self.warehouse.flush_model()
|
||||
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
|
||||
picking_customer = self.env['stock.picking'].create({
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.customer_location,
|
||||
'partner_id': self.env['ir.model.data']._xmlid_to_res_id('base.res_partner_4'),
|
||||
'picking_type_id': self.warehouse.out_type_id.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': self.finished_product.name,
|
||||
'product_id': self.finished_product.id,
|
||||
'product_uom_qty': 2,
|
||||
'picking_id': picking_customer.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.customer_location,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_customer.action_confirm()
|
||||
production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
|
||||
self.assertTrue(production_order)
|
||||
|
||||
move_stock_preprod = self.env['stock.move'].search([
|
||||
('product_id', '=', self.raw_product.id),
|
||||
('location_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
|
||||
('picking_type_id', '=', self.warehouse.pbm_type_id.id)
|
||||
])
|
||||
move_stock_postprod = self.env['stock.move'].search([
|
||||
('product_id', '=', self.finished_product.id),
|
||||
('location_id', '=', self.warehouse.sam_loc_id.id),
|
||||
('location_dest_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('picking_type_id', '=', self.warehouse.sam_type_id.id)
|
||||
])
|
||||
|
||||
self.assertTrue(move_stock_preprod)
|
||||
self.assertTrue(move_stock_postprod)
|
||||
self.assertEqual(move_stock_preprod.state, 'assigned')
|
||||
self.assertEqual(move_stock_postprod.state, 'waiting')
|
||||
|
||||
move_stock_preprod._action_cancel()
|
||||
self.assertEqual(production_order.state, 'confirmed')
|
||||
production_order.action_cancel()
|
||||
self.assertTrue(move_stock_postprod.state, 'cancel')
|
||||
|
||||
def test_no_initial_demand(self):
|
||||
""" Test MO/picking before manufacturing/picking after manufacturing
|
||||
components and move_orig/move_dest. Ensure that everything is created
|
||||
correctly.
|
||||
"""
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.finished_product
|
||||
production_form.picking_type_id = self.warehouse.manu_type_id
|
||||
production = production_form.save()
|
||||
production.move_raw_ids.product_uom_qty = 0
|
||||
production.action_confirm()
|
||||
production.action_assign()
|
||||
self.assertFalse(production.move_raw_ids.move_orig_ids)
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reservation_state, 'assigned')
|
||||
|
||||
def test_manufacturing_3_steps_flexible(self):
|
||||
""" Test MO/picking before manufacturing/picking after manufacturing
|
||||
components and move_orig/move_dest. Ensure that additional moves are put
|
||||
in picking before manufacturing too.
|
||||
"""
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
bom = self.env['mrp.bom'].search([
|
||||
('product_id', '=', self.finished_product.id)
|
||||
])
|
||||
new_product = self.env['product.product'].create({
|
||||
'name': 'New product',
|
||||
'type': 'product',
|
||||
})
|
||||
bom.consumption = 'flexible'
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.finished_product
|
||||
production_form.picking_type_id = self.warehouse.manu_type_id
|
||||
production = production_form.save()
|
||||
|
||||
production.action_confirm()
|
||||
production.is_locked = False
|
||||
production_form = Form(production)
|
||||
with production_form.move_raw_ids.new() as move:
|
||||
move.product_id = new_product
|
||||
move.product_uom_qty = 2
|
||||
production = production_form.save()
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 2)
|
||||
pbm_move = move_raw_ids.move_orig_ids
|
||||
self.assertEqual(len(pbm_move), 2)
|
||||
self.assertTrue(new_product in pbm_move.product_id)
|
||||
|
||||
def test_3_steps_and_byproduct(self):
|
||||
""" Suppose a warehouse with Manufacture option set to '3 setps' and a product P01 with a reordering rule.
|
||||
Suppose P01 has a BoM and this BoM mentions that when some P01 are produced, some P02 are produced too.
|
||||
This test ensures that when a MO is generated thanks to the reordering rule, 2 pickings are also
|
||||
generated:
|
||||
- One to bring the components
|
||||
- Another to return the P01 and P02 produced
|
||||
"""
|
||||
warehouse = self.warehouse
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
warehouse_stock_location = warehouse.lot_stock_id
|
||||
pre_production_location = warehouse.pbm_loc_id
|
||||
post_production_location = warehouse.sam_loc_id
|
||||
|
||||
one_unit_uom = self.env.ref('uom.product_uom_unit')
|
||||
[two_units_uom, four_units_uom] = self.env['uom.uom'].create([{
|
||||
'name': 'x%s' % i,
|
||||
'category_id': self.ref('uom.product_uom_categ_unit'),
|
||||
'uom_type': 'bigger',
|
||||
'factor_inv': i,
|
||||
} for i in [2, 4]])
|
||||
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Super Product',
|
||||
'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))],
|
||||
'type': 'product',
|
||||
})
|
||||
secondary_product = self.env['product.product'].create({
|
||||
'name': 'Secondary',
|
||||
'type': 'product',
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Component',
|
||||
'type': 'consu',
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': two_units_uom.id,
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': one_unit_uom.id,
|
||||
})],
|
||||
'byproduct_ids': [(0, 0, {
|
||||
'product_id': secondary_product.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': four_units_uom.id,
|
||||
})],
|
||||
})
|
||||
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'warehouse_id': warehouse.id,
|
||||
'location_id': warehouse_stock_location.id,
|
||||
'product_id': finished_product.id,
|
||||
'product_min_qty': 2,
|
||||
'product_max_qty': 2,
|
||||
})
|
||||
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
|
||||
pickings = mo.picking_ids
|
||||
self.assertEqual(len(pickings), 2)
|
||||
|
||||
preprod_picking = pickings[0] if pickings[0].location_id == warehouse_stock_location else pickings[1]
|
||||
self.assertEqual(preprod_picking.location_id, warehouse_stock_location)
|
||||
self.assertEqual(preprod_picking.location_dest_id, pre_production_location)
|
||||
|
||||
postprod_picking = pickings - preprod_picking
|
||||
self.assertEqual(postprod_picking.location_id, post_production_location)
|
||||
self.assertEqual(postprod_picking.location_dest_id, warehouse_stock_location)
|
||||
|
||||
byproduct_postprod_move = self.env['stock.move'].search([
|
||||
('product_id', '=', secondary_product.id),
|
||||
('location_id', '=', post_production_location.id),
|
||||
('location_dest_id', '=', warehouse_stock_location.id),
|
||||
])
|
||||
self.assertEqual(byproduct_postprod_move.state, 'waiting')
|
||||
self.assertEqual(byproduct_postprod_move.group_id.name, mo.name)
|
||||
|
||||
def test_manufacturing_3_steps_trigger_reordering_rules(self):
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
|
||||
with Form(self.raw_product) as p:
|
||||
p.route_ids.clear()
|
||||
p.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
|
||||
|
||||
# Create an additional BoM for component
|
||||
product_form = Form(self.env['product.product'])
|
||||
product_form.name = 'Wood'
|
||||
product_form.detailed_type = 'product'
|
||||
product_form.uom_id = self.uom_unit
|
||||
product_form.uom_po_id = self.uom_unit
|
||||
self.wood_product = product_form.save()
|
||||
|
||||
# Create bom for manufactured product
|
||||
bom_product_form = Form(self.env['mrp.bom'])
|
||||
bom_product_form.product_id = self.raw_product
|
||||
bom_product_form.product_tmpl_id = self.raw_product.product_tmpl_id
|
||||
bom_product_form.product_qty = 1.0
|
||||
bom_product_form.type = 'normal'
|
||||
with bom_product_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = self.wood_product
|
||||
bom_line.product_qty = 1.0
|
||||
|
||||
bom_product_form.save()
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.finished_product, self.warehouse.lot_stock_id, -1.0)
|
||||
|
||||
rr_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
rr_form.product_id = self.wood_product
|
||||
rr_form.location_id = self.warehouse.lot_stock_id
|
||||
rr_form.save()
|
||||
|
||||
rr_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
rr_form.product_id = self.finished_product
|
||||
rr_form.location_id = self.warehouse.lot_stock_id
|
||||
rr_finish = rr_form.save()
|
||||
|
||||
rr_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
rr_form.product_id = self.raw_product
|
||||
rr_form.location_id = self.warehouse.lot_stock_id
|
||||
rr_form.save()
|
||||
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
|
||||
pickings_component = self.env['stock.picking'].search(
|
||||
[('product_id', '=', self.wood_product.id)])
|
||||
self.assertTrue(pickings_component)
|
||||
self.assertTrue(rr_finish.name in pickings_component.origin)
|
||||
|
||||
def test_2_steps_and_additional_moves(self):
|
||||
""" Suppose a 2-steps configuration. If a user adds a product to an existing draft MO and then
|
||||
confirms it, the associated picking should includes this new product"""
|
||||
self.warehouse.manufacture_steps = 'pbm'
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.bom.product_id
|
||||
mo_form.picking_type_id = self.warehouse.manu_type_id
|
||||
mo = mo_form.save()
|
||||
|
||||
component_move = mo.move_raw_ids[0]
|
||||
mo.with_context(default_raw_material_production_id=mo.id).move_raw_ids = [
|
||||
[0, 0, {
|
||||
'location_id': component_move.location_id.id,
|
||||
'location_dest_id': component_move.location_dest_id.id,
|
||||
'picking_type_id': component_move.picking_type_id.id,
|
||||
'product_id': self.product_2.id,
|
||||
'name': self.product_2.display_name,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_2.uom_id.id,
|
||||
'warehouse_id': component_move.warehouse_id.id,
|
||||
'raw_material_production_id': mo.id,
|
||||
}]
|
||||
]
|
||||
|
||||
mo.action_confirm()
|
||||
|
||||
self.assertEqual(self.bom.bom_line_ids.product_id + self.product_2, mo.picking_ids.move_ids.product_id)
|
||||
|
||||
def test_manufacturing_complex_product_3_steps(self):
|
||||
""" Test MO/picking after manufacturing a complex product which uses
|
||||
manufactured components. Ensure that everything is created and picked
|
||||
correctly.
|
||||
"""
|
||||
|
||||
self.warehouse.mto_pull_id.route_id.active = True
|
||||
# Creating complex product which trigger another manifacture
|
||||
|
||||
routes = self.warehouse.manufacture_pull_id.route_id + self.warehouse.mto_pull_id.route_id
|
||||
self.complex_product = self.env['product.product'].create({
|
||||
'name': 'Arrow',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, routes.ids)],
|
||||
})
|
||||
|
||||
# Create raw product for manufactured product
|
||||
self.raw_product_2 = self.env['product.product'].create({
|
||||
'name': 'Raw Iron',
|
||||
'type': 'product',
|
||||
'uom_id': self.uom_unit.id,
|
||||
'uom_po_id': self.uom_unit.id,
|
||||
})
|
||||
|
||||
self.finished_product.route_ids = [(6, 0, routes.ids)]
|
||||
|
||||
# Create bom for manufactured product
|
||||
bom_product_form = Form(self.env['mrp.bom'])
|
||||
bom_product_form.product_id = self.complex_product
|
||||
bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id
|
||||
with bom_product_form.bom_line_ids.new() as line:
|
||||
line.product_id = self.finished_product
|
||||
line.product_qty = 1.0
|
||||
with bom_product_form.bom_line_ids.new() as line:
|
||||
line.product_id = self.raw_product_2
|
||||
line.product_qty = 1.0
|
||||
|
||||
self.complex_bom = bom_product_form.save()
|
||||
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.complex_product
|
||||
production_form.picking_type_id = self.warehouse.manu_type_id
|
||||
production = production_form.save()
|
||||
production.action_confirm()
|
||||
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 2)
|
||||
sfp_move_raw_id, raw_move_raw_id = move_raw_ids
|
||||
self.assertEqual(sfp_move_raw_id.product_id, self.finished_product)
|
||||
self.assertEqual(raw_move_raw_id.product_id, self.raw_product_2)
|
||||
|
||||
for move_raw_id in move_raw_ids:
|
||||
self.assertEqual(move_raw_id.picking_type_id, self.warehouse.manu_type_id)
|
||||
|
||||
pbm_move = move_raw_id.move_orig_ids
|
||||
self.assertEqual(len(pbm_move), 1)
|
||||
self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
|
||||
self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
|
||||
self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
|
||||
|
||||
# Check move locations
|
||||
move_finished_ids = production.move_finished_ids
|
||||
self.assertEqual(len(move_finished_ids), 1)
|
||||
self.assertEqual(move_finished_ids.product_id, self.complex_product)
|
||||
self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
|
||||
sam_move = move_finished_ids.move_dest_ids
|
||||
self.assertEqual(len(sam_move), 1)
|
||||
self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
|
||||
self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
|
||||
self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
|
||||
self.assertFalse(sam_move.move_dest_ids)
|
||||
|
||||
subproduction = self.env['mrp.production'].browse(production.id+1)
|
||||
sfp_pickings = subproduction.picking_ids.sorted('id')
|
||||
|
||||
# SFP Production: 2 pickings, 1 group
|
||||
self.assertEqual(len(sfp_pickings), 2)
|
||||
self.assertEqual(sfp_pickings.mapped('group_id'), subproduction.procurement_group_id)
|
||||
|
||||
# Move Raw Stick - Stock -> Preprocessing
|
||||
picking = sfp_pickings[0]
|
||||
self.assertEqual(len(picking.move_ids), 1)
|
||||
picking.move_ids[0].product_id = self.raw_product
|
||||
|
||||
# Move SFP - PostProcessing -> Stock
|
||||
picking = sfp_pickings[1]
|
||||
self.assertEqual(len(picking.move_ids), 1)
|
||||
picking.move_ids[0].product_id = self.finished_product
|
||||
|
||||
# Main production 2 pickings, 1 group
|
||||
pickings = production.picking_ids.sorted('id')
|
||||
self.assertEqual(len(pickings), 2)
|
||||
self.assertEqual(pickings.mapped('group_id'), production.procurement_group_id)
|
||||
|
||||
# Move 2 components Stock -> Preprocessing
|
||||
picking = pickings[0]
|
||||
self.assertEqual(len(picking.move_ids), 2)
|
||||
picking.move_ids[0].product_id = self.finished_product
|
||||
picking.move_ids[1].product_id = self.raw_product_2
|
||||
|
||||
# Move FP PostProcessing -> Stock
|
||||
picking = pickings[1]
|
||||
self.assertEqual(len(picking.move_ids), 1)
|
||||
picking.product_id = self.complex_product
|
||||
|
||||
def test_child_parent_relationship_on_backorder_creation(self):
|
||||
""" Test Child Mo and Source Mo in 2/3-step production for reorder
|
||||
rules in backorder using order points with the help of run scheduler """
|
||||
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
|
||||
rr_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
rr_form.product_id = self.finished_product
|
||||
rr_form.product_min_qty = 20
|
||||
rr_form.product_max_qty = 40
|
||||
rr_form.save()
|
||||
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
|
||||
mo = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 20
|
||||
mo = mo_form.save()
|
||||
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
self.assertEqual(mo.mrp_production_child_count, 0, "Children MOs counted as existing where there should be none")
|
||||
self.assertEqual(mo.mrp_production_source_count, 0, "Source MOs counted as existing where there should be none")
|
||||
self.assertEqual(mo.mrp_production_backorder_count, 2)
|
||||
|
||||
def test_source_location_on_merge_mo_3_steps(self):
|
||||
"""Check that default values are correct after merging mos when 3-step manufacturing"""
|
||||
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
|
||||
# picking with non default location
|
||||
picking_type = self.env['stock.picking.type'].create({
|
||||
'name': 'Manufacturing',
|
||||
'code': 'mrp_operation',
|
||||
'warehouse_id': warehouse.id,
|
||||
'default_location_src_id': self.warehouse.pbm_loc_id.copy().id,
|
||||
'default_location_dest_id': self.warehouse.sam_loc_id.copy().id,
|
||||
'sequence_code': 'TMP',
|
||||
'sequence_id': self.env['ir.sequence'].create({
|
||||
'code': 'mrp.production',
|
||||
'name': 'tmp_production_sequence',
|
||||
}).id,
|
||||
})
|
||||
|
||||
mo1_form = Form(self.env['mrp.production'])
|
||||
mo1_form.product_id = self.finished_product
|
||||
mo1_form.picking_type_id = picking_type
|
||||
mo1 = mo1_form.save()
|
||||
mo1.action_confirm()
|
||||
|
||||
mo2_form = Form(self.env['mrp.production'])
|
||||
mo2_form.product_id = self.finished_product
|
||||
mo2_form.picking_type_id = picking_type
|
||||
mo2 = mo2_form.save()
|
||||
mo2.action_confirm()
|
||||
|
||||
action = (mo1 + mo2).action_merge()
|
||||
mo = self.env[action['res_model']].browse(action['res_id'])
|
||||
|
||||
self.assertEqual(picking_type.default_location_src_id, mo.move_raw_ids.location_id,
|
||||
"The default source location of the merged mo should be the same as the 1st of the original MOs")
|
||||
self.assertEqual(picking_type, mo.picking_type_id,
|
||||
"The operation type of the merged mo should be the same as the 1st of the original MOs")
|
||||
|
||||
def test_manufacturing_bom_from_reordering_rules(self):
|
||||
"""
|
||||
Check that the manufacturing order is created with the BoM set in the reording rule:
|
||||
- Create a product with 2 bill of materials,
|
||||
- Create an orderpoint for this product specifying the 2nd BoM that must be used,
|
||||
- Check that the MO has been created with the 2nd BoM
|
||||
"""
|
||||
manufacturing_route = self.env['stock.rule'].search([
|
||||
('action', '=', 'manufacture')]).route_id
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Product',
|
||||
'type': 'product',
|
||||
'route_ids': manufacturing_route,
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': finished_product.uom_id.id,
|
||||
'type': 'normal',
|
||||
})
|
||||
bom_2 = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': finished_product.uom_id.id,
|
||||
'type': 'normal',
|
||||
})
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Orderpoint for P1',
|
||||
'product_id': self.finished_product.id,
|
||||
'product_min_qty': 1,
|
||||
'product_max_qty': 1,
|
||||
'route_id': manufacturing_route.id,
|
||||
'bom_id': bom_2.id,
|
||||
})
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
mo = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
self.assertEqual(mo.product_qty, 1.0)
|
||||
self.assertEqual(mo.bom_id, bom_2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue