mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-24 07:12:02 +02:00
19.0 vanilla
This commit is contained in:
parent
accf5918df
commit
6e65e8c877
688 changed files with 225434 additions and 199401 deletions
|
|
@ -4,16 +4,19 @@ from . import test_bom
|
|||
from . import test_byproduct
|
||||
from . import test_cancel_mo
|
||||
from . import test_order
|
||||
from . import test_quant
|
||||
from . import test_stock
|
||||
from . import test_stock_report
|
||||
from . import test_warehouse_multistep_manufacturing
|
||||
from . import test_procurement
|
||||
from . import test_replenish
|
||||
from . import test_unbuild
|
||||
from . import test_oee
|
||||
from . import test_traceability
|
||||
from . import test_multicompany
|
||||
from . import test_backorder
|
||||
from . import test_smp
|
||||
from . import test_performance
|
||||
from . import test_consume_tracked_component
|
||||
from . import test_consume_component
|
||||
from . import test_manual_consumption
|
||||
from . import test_workcenter
|
||||
from . import test_mrp_reports
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import Form
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.stock.tests import common2
|
||||
from odoo.addons.stock.tests.common import TestStockCommon
|
||||
|
||||
|
||||
class TestMrpCommon(common2.TestStockCommon):
|
||||
class TestMrpCommon(TestStockCommon):
|
||||
|
||||
@classmethod
|
||||
def generate_mo(cls, tracking_final='none', tracking_base_1='none', tracking_base_2='none', qty_final=5, qty_base_1=4, qty_base_2=1, picking_type_id=False, consumption=False):
|
||||
|
|
@ -14,21 +15,24 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
the tracking/qty for each different products. It returns the
|
||||
MO, used bom and the tree products.
|
||||
"""
|
||||
product_to_build = cls.env['product.product'].create({
|
||||
'name': 'Young Tom',
|
||||
'type': 'product',
|
||||
'tracking': tracking_final,
|
||||
})
|
||||
product_to_use_1 = cls.env['product.product'].create({
|
||||
'name': 'Botox',
|
||||
'type': 'product',
|
||||
'tracking': tracking_base_1,
|
||||
})
|
||||
product_to_use_2 = cls.env['product.product'].create({
|
||||
'name': 'Old Tom',
|
||||
'type': 'product',
|
||||
'tracking': tracking_base_2,
|
||||
})
|
||||
product_to_build, product_to_use_1, product_to_use_2 = cls.env['product.product'].create([
|
||||
{
|
||||
'name': 'Young Tom',
|
||||
'type': 'consu',
|
||||
'is_storable': True,
|
||||
'tracking': tracking_final,
|
||||
}, {
|
||||
'name': 'Botox',
|
||||
'type': 'consu',
|
||||
'is_storable': True,
|
||||
'tracking': tracking_base_1,
|
||||
}, {
|
||||
'name': 'Old Tom',
|
||||
'type': 'consu',
|
||||
'is_storable': True,
|
||||
'tracking': tracking_base_2,
|
||||
}
|
||||
])
|
||||
bom_1 = cls.env['mrp.bom'].create({
|
||||
'product_id': product_to_build.id,
|
||||
'product_tmpl_id': product_to_build.product_tmpl_id.id,
|
||||
|
|
@ -37,9 +41,10 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
'type': 'normal',
|
||||
'consumption': consumption if consumption else 'flexible',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_to_use_2.id, 'product_qty': qty_base_2}),
|
||||
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': qty_base_1})
|
||||
]})
|
||||
Command.create({'product_id': product_to_use_2.id, 'product_qty': qty_base_2}),
|
||||
Command.create({'product_id': product_to_use_1.id, 'product_qty': qty_base_1}),
|
||||
],
|
||||
})
|
||||
mo_form = Form(cls.env['mrp.production'])
|
||||
mo_form.product_id = product_to_build
|
||||
if picking_type_id:
|
||||
|
|
@ -52,7 +57,26 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpCommon, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
|
||||
cls.group_mrp_routings = cls.quick_ref('mrp.group_mrp_routings')
|
||||
|
||||
# Kept for reduced diff in existing tests, should be dropped someday
|
||||
cls.product_7_template = cls.product_template_sofa
|
||||
|
||||
cls.product_7_attr1_v1 = cls.product_7_template.attribute_line_ids[
|
||||
0].product_template_value_ids[0]
|
||||
cls.product_7_attr1_v2 = cls.product_7_template.attribute_line_ids[
|
||||
0].product_template_value_ids[1]
|
||||
cls.product_7_attr1_v3 = cls.product_7_template.attribute_line_ids[
|
||||
0].product_template_value_ids[2]
|
||||
|
||||
cls.product_7_1 = cls.product_7_template._get_variant_for_combination(
|
||||
cls.product_7_attr1_v1)
|
||||
cls.product_7_2 = cls.product_7_template._get_variant_for_combination(
|
||||
cls.product_7_attr1_v2)
|
||||
cls.product_7_3 = cls.product_7_template._get_variant_for_combination(
|
||||
cls.product_7_attr1_v3)
|
||||
|
||||
(
|
||||
cls.product_4,
|
||||
|
|
@ -62,7 +86,6 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
) = cls.env['product.product'].create([{
|
||||
'name': 'Stick', # product_4
|
||||
'uom_id': cls.uom_dozen.id,
|
||||
'uom_po_id': cls.uom_dozen.id,
|
||||
}, {
|
||||
'name': 'Stone Tools', # product_5
|
||||
}, {
|
||||
|
|
@ -73,7 +96,8 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
|
||||
# Update demo products
|
||||
(cls.product_2 | cls.product_3 | cls.product_4 | cls.product_5 | cls.product_6 | cls.product_7_3 | cls.product_8).write({
|
||||
'type': 'product',
|
||||
'type': 'consu',
|
||||
'is_storable': True,
|
||||
})
|
||||
|
||||
# User Data: mrp user and mrp manager
|
||||
|
|
@ -93,32 +117,41 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
notification_type='inbox',
|
||||
groups='mrp.group_mrp_manager, stock.group_stock_user, mrp.group_mrp_byproducts, uom.group_uom',
|
||||
)
|
||||
# Required for `product_uom_id` to be visible in the view
|
||||
# This class is used by a lot of tests which sets `product_uom_id` on `mrp.production`
|
||||
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
|
||||
|
||||
cls.workcenter_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter',
|
||||
'default_capacity': 2,
|
||||
'time_start': 10,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
})
|
||||
cls.workcenter_2 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Simple Workcenter',
|
||||
'default_capacity': 1,
|
||||
'time_start': 0,
|
||||
'time_stop': 0,
|
||||
'time_efficiency': 100,
|
||||
})
|
||||
cls.workcenter_3 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Double Workcenter',
|
||||
'default_capacity': 2,
|
||||
'time_start': 0,
|
||||
'time_stop': 0,
|
||||
'time_efficiency': 100,
|
||||
})
|
||||
# Both groups below are required to make fields `product_uom_id` and
|
||||
# `workorder_ids` to be visible in the view of `mrp.production`. The
|
||||
# field `product_uom_id` must be set by many tests, and subviews of
|
||||
# `workorder_ids` must be present in many tests to create records.
|
||||
cls.env.user.group_ids += cls.group_uom + cls.group_mrp_routings
|
||||
cls.picking_type_manu = cls.warehouse_1.manu_type_id
|
||||
cls.picking_type_manu.sequence = 5
|
||||
cls.route_manufacture = cls.warehouse_1.manufacture_pull_id.route_id
|
||||
|
||||
cls.workcenter_1, cls.workcenter_2, cls.workcenter_3 = cls.env['mrp.workcenter'].create([
|
||||
{
|
||||
'name': 'Nuclear Workcenter',
|
||||
'time_start': 10,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
}, {
|
||||
'name': 'Simple Workcenter',
|
||||
'time_start': 0,
|
||||
'time_stop': 0,
|
||||
'time_efficiency': 100,
|
||||
}, {
|
||||
'name': 'Double Workcenter',
|
||||
'time_start': 0,
|
||||
'time_stop': 0,
|
||||
'time_efficiency': 100,
|
||||
}
|
||||
])
|
||||
for (workcenter, default_capacity) in [(cls.workcenter_1, 2), (cls.workcenter_2, 1), (cls.workcenter_3, 2)]:
|
||||
cls.env['mrp.workcenter.capacity'].create({
|
||||
'workcenter_id': workcenter.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'capacity': default_capacity,
|
||||
'time_start': workcenter.time_start,
|
||||
'time_stop': workcenter.time_stop,
|
||||
})
|
||||
cls.bom_1 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_4.id,
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
|
|
@ -129,8 +162,8 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
|
||||
Command.create({'product_id': cls.product_2.id, 'product_qty': 2}),
|
||||
Command.create({'product_id': cls.product_1.id, 'product_qty': 4}),
|
||||
]})
|
||||
cls.bom_2 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_5.id,
|
||||
|
|
@ -139,13 +172,13 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
Command.create({'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
'type': 'phantom',
|
||||
'sequence': 2,
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_3.id, 'product_qty': 3})
|
||||
Command.create({'product_id': cls.product_4.id, 'product_qty': 2}),
|
||||
Command.create({'product_id': cls.product_3.id, 'product_qty': 3}),
|
||||
]})
|
||||
cls.bom_3 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_6.id,
|
||||
|
|
@ -155,88 +188,65 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
'consumption': 'flexible',
|
||||
'product_qty': 2.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
(0, 0, {'name': 'Weld Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
|
||||
Command.create({'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
Command.create({'name': 'Weld Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_5.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}),
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 12})
|
||||
Command.create({'product_id': cls.product_5.id, 'product_qty': 2}),
|
||||
Command.create({'product_id': cls.product_4.id, 'product_qty': 8}),
|
||||
Command.create({'product_id': cls.product_2.id, 'product_qty': 12}),
|
||||
]})
|
||||
cls.bom_4 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Rub it gently with a cloth', 'workcenter_id': cls.workcenter_2.id,
|
||||
'time_mode_batch': 1, 'time_mode': "auto", 'sequence': 1}),
|
||||
],
|
||||
'operation_ids': [Command.create({
|
||||
'name': 'Rub it gently with a cloth',
|
||||
'workcenter_id': cls.workcenter_2.id,
|
||||
'time_mode_batch': 1,
|
||||
'time_mode': "auto",
|
||||
'sequence': 1,
|
||||
})],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
]})
|
||||
cls.bom_5 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Rub it gently with a cloth two at once', 'workcenter_id': cls.workcenter_3.id,
|
||||
'time_mode_batch': 2, 'time_mode': "auto", 'sequence': 1}),
|
||||
],
|
||||
'operation_ids': [Command.create({
|
||||
'name': 'Rub it gently with a cloth two at once',
|
||||
'workcenter_id': cls.workcenter_3.id,
|
||||
'time_mode_batch': 2,
|
||||
'time_mode': "auto",
|
||||
'sequence': 1,
|
||||
})],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
]})
|
||||
cls.bom_6 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Rub it gently with a cloth two at once', 'workcenter_id': cls.workcenter_3.id,
|
||||
'time_mode_batch': 1, 'time_mode': "auto", 'sequence': 1}),
|
||||
],
|
||||
'operation_ids': [Command.create({
|
||||
'name': 'Rub it gently with a cloth two at once',
|
||||
'workcenter_id': cls.workcenter_3.id,
|
||||
'time_mode_batch': 1,
|
||||
'time_mode': "auto",
|
||||
'sequence': 1,
|
||||
})],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': cls.product_1.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
cls.stock_location_14 = cls.env['stock.location'].create({
|
||||
'name': 'Shelf 2',
|
||||
'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
|
||||
})
|
||||
cls.stock_location_components = cls.env['stock.location'].create({
|
||||
'name': 'Shelf 1',
|
||||
'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
|
||||
})
|
||||
cls.laptop = cls.env['product.product'].create({
|
||||
'name': 'Acoustic Bloc Screens',
|
||||
'uom_id': cls.env.ref("uom.product_uom_unit").id,
|
||||
'uom_po_id': cls.env.ref("uom.product_uom_unit").id,
|
||||
'type': 'product',
|
||||
'tracking': 'none',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.graphics_card = cls.env['product.product'].create({
|
||||
'name': 'Individual Workplace',
|
||||
'uom_id': cls.env.ref("uom.product_uom_unit").id,
|
||||
'uom_po_id': cls.env.ref("uom.product_uom_unit").id,
|
||||
'type': 'product',
|
||||
'tracking': 'none',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def make_prods(cls, n):
|
||||
return [
|
||||
cls.env["product.product"].create(
|
||||
{"name": f"p{k + 1}", "type": "product"}
|
||||
)
|
||||
for k in range(n)
|
||||
]
|
||||
cls.stock_location_components = cls.shelf_1
|
||||
|
||||
@classmethod
|
||||
def make_bom(cls, p, *cs):
|
||||
|
|
@ -248,7 +258,7 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
"type": "phantom",
|
||||
"product_uom_id": cls.uom_unit.id,
|
||||
"bom_line_ids": [
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
"product_id": c.id,
|
||||
"product_qty": 1,
|
||||
"product_uom_id": cls.uom_unit.id
|
||||
|
|
@ -257,3 +267,17 @@ class TestMrpCommon(common2.TestStockCommon):
|
|||
],
|
||||
}
|
||||
)
|
||||
|
||||
def full_availability(self):
|
||||
"""set full availability for all calendars"""
|
||||
calendar = self.env['resource.calendar'].search([])
|
||||
calendar.write({'attendance_ids': [(5, 0, 0)]})
|
||||
calendar.write({'attendance_ids': [
|
||||
(0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Saturday', 'dayofweek': '5', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Sunday', 'dayofweek': '6', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
]})
|
||||
|
|
|
|||
|
|
@ -1,239 +0,0 @@
|
|||
import copy
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import common, Form
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
class TestConsumeTrackedComponentCommon(common.TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
The following variables are used in each test to define the number of MO to generate.
|
||||
They're also used as a verification in the executeConsumptionTriggers() to see if enough MO were passed to it
|
||||
in order to test all the triggers.
|
||||
|
||||
SERIAL : MO's product_tracking is 'serial'
|
||||
DEFAULT : MO's product_tracking is 'none' or 'lot'
|
||||
AVAILABLE : MO'S raw components are fully available
|
||||
"""
|
||||
super().setUpClass()
|
||||
|
||||
cls.SERIAL_AVAILABLE_TRIGGERS_COUNT = 3
|
||||
cls.DEFAULT_AVAILABLE_TRIGGERS_COUNT = 2
|
||||
cls.SERIAL_TRIGGERS_COUNT = 2
|
||||
cls.DEFAULT_TRIGGERS_COUNT = 1
|
||||
|
||||
cls.manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture')
|
||||
cls.stock_id = cls.env.ref('stock.stock_location_stock').id
|
||||
|
||||
cls.picking_type = cls.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
|
||||
cls.picking_type.use_create_components_lots = True
|
||||
cls.picking_type.use_auto_consume_components_lots = True
|
||||
|
||||
#Create Products & Components
|
||||
cls.produced_lot = cls.env['product.product'].create({
|
||||
'name': 'Produced Lot',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'lot',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
cls.produced_serial = cls.env['product.product'].create({
|
||||
'name': 'Produced Serial',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'serial',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
cls.produced_none = cls.env['product.product'].create({
|
||||
'name': 'Produced None',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'none',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
|
||||
cls.raw_lot = cls.env['product.product'].create({
|
||||
'name': 'Raw Lot',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'lot',
|
||||
})
|
||||
cls.raw_serial = cls.env['product.product'].create({
|
||||
'name': 'Raw Serial',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'serial',
|
||||
})
|
||||
cls.raw_none = cls.env['product.product'].create({
|
||||
'name': 'Raw None',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
'tracking' : 'none',
|
||||
})
|
||||
|
||||
cls.raws = [cls.raw_none, cls.raw_lot, cls.raw_serial]
|
||||
|
||||
#Workcenter
|
||||
cls.workcenter = cls.env['mrp.workcenter'].create({
|
||||
'name' : 'Assembly Line',
|
||||
})
|
||||
|
||||
#BoMs
|
||||
cls.bom_none = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id' : cls.produced_none.product_tmpl_id.id,
|
||||
'product_uom_id' : cls.produced_none.uom_id.id,
|
||||
'consumption' : 'flexible',
|
||||
'sequence' : 1
|
||||
})
|
||||
|
||||
cls.bom_none_lines = cls.create_bom_lines(cls.bom_none, cls.raws, [3, 2, 1])
|
||||
|
||||
cls.bom_lot = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id' : cls.produced_lot.product_tmpl_id.id,
|
||||
'product_uom_id' : cls.produced_lot.uom_id.id,
|
||||
'consumption' : 'flexible',
|
||||
'sequence' : 2
|
||||
})
|
||||
|
||||
cls.bom_lot_lines = cls.create_bom_lines(cls.bom_lot, cls.raws, [3, 2, 1])
|
||||
|
||||
cls.bom_serial = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id' : cls.produced_serial.product_tmpl_id.id,
|
||||
'product_uom_id' : cls.produced_serial.uom_id.id,
|
||||
'consumption' : 'flexible',
|
||||
'sequence' : 1
|
||||
})
|
||||
|
||||
cls.bom_serial_lines = cls.create_bom_lines(cls.bom_serial, cls.raws, [3, 2, 1])
|
||||
|
||||
#Manufacturing Orders
|
||||
cls.mo_none_tmpl = {
|
||||
'product_id' : cls.produced_none.id,
|
||||
'product_uom_id' : cls.produced_none.uom_id.id,
|
||||
'product_qty' : 1,
|
||||
'bom_id' : cls.bom_none.id
|
||||
}
|
||||
|
||||
cls.mo_lot_tmpl = {
|
||||
'product_id' : cls.produced_lot.id,
|
||||
'product_uom_id' : cls.produced_lot.uom_id.id,
|
||||
'product_qty' : 1,
|
||||
'bom_id' : cls.bom_lot.id
|
||||
}
|
||||
|
||||
cls.mo_serial_tmpl = {
|
||||
'product_id' : cls.produced_serial.id,
|
||||
'product_uom_id' : cls.produced_serial.uom_id.id,
|
||||
'product_qty' : 1,
|
||||
'bom_id' : cls.bom_serial.id
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_quant(cls, product, qty, offset=0, name="L"):
|
||||
i = 1
|
||||
if product.tracking == 'serial':
|
||||
i, qty = qty, 1
|
||||
if name == "L":
|
||||
name = "S"
|
||||
|
||||
vals = []
|
||||
for x in range(1, i+1):
|
||||
qDict = {
|
||||
'location_id': cls.stock_id,
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': qty,
|
||||
}
|
||||
|
||||
if product.tracking != 'none':
|
||||
qDict['lot_id'] = cls.env['stock.lot'].create({
|
||||
'name': name + str(offset + x),
|
||||
'product_id': product.id,
|
||||
'company_id': cls.env.company.id
|
||||
}).id
|
||||
vals.append(qDict)
|
||||
|
||||
return cls.env['stock.quant'].create(vals)
|
||||
|
||||
@classmethod
|
||||
def create_bom_lines(cls, bom, products, quantities=None):
|
||||
if quantities is None:
|
||||
quantities = [1 for i in range(len(products))]
|
||||
|
||||
vals = []
|
||||
for product, seq in zip(products, range(len(products))):
|
||||
vals.append({
|
||||
'product_id' : product.id,
|
||||
'product_qty' : quantities[seq],
|
||||
'product_uom_id' : product.uom_id.id,
|
||||
'sequence' : seq,
|
||||
'bom_id' : bom.id,
|
||||
})
|
||||
|
||||
return cls.env['mrp.bom.line'].create(vals)
|
||||
|
||||
@classmethod
|
||||
def create_mo(cls, template, count):
|
||||
vals = []
|
||||
for _ in range(count):
|
||||
vals.append(copy.deepcopy(template))
|
||||
return cls.env['mrp.production'].create(vals)
|
||||
|
||||
def executeConsumptionTriggers(self, mrp_productions):
|
||||
"""
|
||||
There's 3 different triggers to test : _onchange_producing(), action_generate_serial(), button_mark_done().
|
||||
|
||||
Depending on the tracking of the final product and the availability of the components,
|
||||
only a part of these 3 triggers is available or intended to work.
|
||||
|
||||
This function automatically call and process the appropriate triggers.
|
||||
"""
|
||||
tracking = mrp_productions[0].product_tracking
|
||||
sameTracking = True
|
||||
for mo in mrp_productions:
|
||||
sameTracking = sameTracking and mo.product_tracking == tracking
|
||||
self.assertTrue(sameTracking, "MOs passed to the executeConsumptionTriggers method shall have the same product_tracking")
|
||||
|
||||
isSerial = tracking == 'serial'
|
||||
isAvailable = all(move.state == 'assigned' for move in mrp_productions.move_raw_ids)
|
||||
|
||||
countOk = True
|
||||
length = len(mrp_productions)
|
||||
if isSerial:
|
||||
if isAvailable:
|
||||
countOk = length == self.SERIAL_AVAILABLE_TRIGGERS_COUNT
|
||||
else:
|
||||
countOk = length == self.SERIAL_TRIGGERS_COUNT
|
||||
else:
|
||||
if isAvailable:
|
||||
countOk = length == self.DEFAULT_AVAILABLE_TRIGGERS_COUNT
|
||||
else:
|
||||
countOk = length == self.DEFAULT_TRIGGERS_COUNT
|
||||
self.assertTrue(countOk, "The number of MOs passed to the executeConsumptionTriggers method does not match the associated TRIGGERS_COUNT")
|
||||
|
||||
mrp_productions[0].qty_producing = mrp_productions[0].product_qty
|
||||
mrp_productions[0]._onchange_producing()
|
||||
|
||||
i = 1
|
||||
if isSerial:
|
||||
mrp_productions[i].action_generate_serial()
|
||||
i += 1
|
||||
|
||||
if isAvailable:
|
||||
mark_done_action = mrp_productions[i].button_mark_done()
|
||||
immediate_production_wizard = Form(
|
||||
self.env['mrp.immediate.production']
|
||||
.with_context(**mark_done_action['context'])
|
||||
).save()
|
||||
error = False
|
||||
has_zero_tracked_component = not mrp_productions[i].picking_type_id.use_auto_consume_components_lots and \
|
||||
any(m.state not in ['done', 'cancel'] and m.has_tracking != 'none' and float_is_zero(m.quantity_done, m.product_uom.rounding) for m in mrp_productions[i].move_raw_ids)
|
||||
try:
|
||||
immediate_production_wizard.process()
|
||||
except UserError:
|
||||
error = True
|
||||
if has_zero_tracked_component:
|
||||
self.assertTrue(error, "Immediate Production Wizard shall raise an error.")
|
||||
else:
|
||||
self.assertFalse(error, "Immediate Production Wizard shall not raise an error.")
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import Form
|
||||
from odoo.tests import common
|
||||
from odoo.exceptions import ValidationError
|
||||
|
|
@ -19,7 +20,7 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
def create_product(name, route_ids=[]):
|
||||
return cls.env['product.product'].create({
|
||||
'name': name,
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'route_ids': route_ids})
|
||||
|
||||
# Create product A, B, C.
|
||||
|
|
@ -35,6 +36,19 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
'bom_line_ids': [(0, 0, {'product_id': cls.product_c_id, 'product_uom_id': cls.uom_unit_id, 'product_qty': 2})],
|
||||
'byproduct_ids': [(0, 0, {'product_id': cls.product_b.id, 'product_uom_id': cls.uom_unit_id, 'product_qty': 1})]
|
||||
})
|
||||
cls.produced_serial = cls.env['product.product'].create({
|
||||
'name': 'Produced Serial',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
cls.sn_1 = cls.env['stock.lot'].create({
|
||||
'name': 'Serial_01',
|
||||
'product_id': cls.produced_serial.id
|
||||
})
|
||||
cls.sn_2 = cls.env['stock.lot'].create({
|
||||
'name': 'Serial_02',
|
||||
'product_id': cls.produced_serial.id
|
||||
})
|
||||
|
||||
def test_00_mrp_byproduct(self):
|
||||
""" Test by product with production order."""
|
||||
|
|
@ -105,7 +119,7 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
mnf_product_a.action_confirm()
|
||||
self.assertEqual(mnf_product_a.state, "confirmed")
|
||||
mnf_product_a.move_raw_ids._action_assign()
|
||||
mnf_product_a.move_raw_ids.quantity_done = mnf_product_a.move_raw_ids.product_uom_qty
|
||||
mnf_product_a.move_raw_ids.picked = True
|
||||
mnf_product_a.move_raw_ids._action_done()
|
||||
self.assertEqual(mnf_product_a.state, "progress")
|
||||
mnf_product_a.qty_producing = 2
|
||||
|
|
@ -169,9 +183,9 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
are included in the `move_finished_ids`. `move_byproduct_ids` is a subset of `move_finished_ids`.
|
||||
|
||||
So, when creating a manufacturing order, whether:
|
||||
- Only `move_finished_ids` is passed, containing both the finished product and the by-products of the BOM,
|
||||
- Only `move_byproduct_ids` is passed, only containing the by-products of the BOM,
|
||||
- Both `move_finished_ids` and `move_byproduct_ids` are passed,
|
||||
- Only `move_finished_ids`(A + B) is passed, containing both the finished product and the by-products of the BOM,
|
||||
- Both `move_finished_ids`(A + B) and `move_byproduct_ids`(B) are passed,
|
||||
- Both `move_finished_ids`(A) and `move_byproduct_ids`(B) are passed,
|
||||
holding the product finished and the byproducts respectively
|
||||
At the end, in the created manufacturing order
|
||||
`move_finished_ids` must contain both the finished product, and the by-products,
|
||||
|
|
@ -215,8 +229,22 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
}),
|
||||
],
|
||||
}),
|
||||
# Only `move_byproduct_ids` passed, containing the by-product move only
|
||||
# Both `move_finished_ids`(A + B) and `move_byproduct_ids`(B) passed,
|
||||
(2.0, 4.0, {
|
||||
'move_finished_ids': [
|
||||
(0, 0, {
|
||||
'product_id': self.product_a.id,
|
||||
'product_uom_qty': 2.0,
|
||||
'location_id': self.product_a.property_stock_production,
|
||||
'location_dest_id': self.warehouse.lot_stock_id.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'product_id': self.product_b.id,
|
||||
'product_uom_qty': 4.0,
|
||||
'location_id': self.product_a.property_stock_production,
|
||||
'location_dest_id': self.warehouse.lot_stock_id.id,
|
||||
}),
|
||||
],
|
||||
'move_byproduct_ids': [
|
||||
(0, 0, {
|
||||
'product_id': self.product_b.id,
|
||||
|
|
@ -226,7 +254,7 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
}),
|
||||
],
|
||||
}),
|
||||
# Both `move_finished_ids` and `move_byproduct_ids` passed,
|
||||
# Both `move_finished_ids`(A) and `move_byproduct_ids`(B) passed,
|
||||
# containing respectively the finished product and the by-product
|
||||
(3.0, 4.0, {
|
||||
'move_finished_ids': [
|
||||
|
|
@ -305,6 +333,7 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
'location_in_id': self.stock_location.id,
|
||||
'location_out_id': self.stock_location.id,
|
||||
'storage_category_id': stor_category.id,
|
||||
'sublocation': 'closest_location',
|
||||
})
|
||||
self.env['stock.putaway.rule'].create({
|
||||
'product_id': self.product_a.id,
|
||||
|
|
@ -344,45 +373,39 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
# Create product
|
||||
self.product_d = self.env['product.product'].create({
|
||||
'name': 'Product D',
|
||||
'type': 'product'})
|
||||
'is_storable': True})
|
||||
self.product_e = self.env['product.product'].create({
|
||||
'name': 'Product E',
|
||||
'type': 'product'})
|
||||
'is_storable': True})
|
||||
|
||||
# Create byproduct
|
||||
byproduct_1 = self.env['stock.move'].create({
|
||||
'name': 'By Product 1',
|
||||
'product_id': self.product_d.id,
|
||||
'product_uom': self.ref('uom.product_uom_unit'),
|
||||
'production_id': mo.id,
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_output'),
|
||||
'product_uom_qty': 0,
|
||||
'quantity_done': 0
|
||||
})
|
||||
byproduct_2 = self.env['stock.move'].create({
|
||||
'name': 'By Product 2',
|
||||
'product_id': self.product_e.id,
|
||||
'product_uom': self.ref('uom.product_uom_unit'),
|
||||
'production_id': mo.id,
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_output'),
|
||||
'product_uom_qty': 0,
|
||||
'quantity_done': 0
|
||||
})
|
||||
|
||||
# Update byproduct has cost share > 100%
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
with self.assertRaises(ValidationError):
|
||||
byproduct_1.cost_share = 120
|
||||
mo.write({'move_byproduct_ids': [(4, byproduct_1.id)]})
|
||||
|
||||
# Update byproduct has cost share < 0%
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
with self.assertRaises(ValidationError):
|
||||
byproduct_1.cost_share = -10
|
||||
mo.write({'move_byproduct_ids': [(4, byproduct_1.id)]})
|
||||
|
||||
# Update byproducts have total cost share > 100%
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
with self.assertRaises(ValidationError):
|
||||
byproduct_1.cost_share = 60
|
||||
byproduct_2.cost_share = 70
|
||||
mo.write({'move_byproduct_ids': [(6, 0, [byproduct_1.id, byproduct_2.id])]})
|
||||
|
|
@ -404,18 +427,142 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(mo.state, 'to_close')
|
||||
mo.move_byproduct_ids[0].quantity_done = 1
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
||||
def test_01_check_byproducts_update(self):
|
||||
"""
|
||||
Test that check byproducts update in stock move should also reflect in stock move line(Product moves).
|
||||
"""
|
||||
# Create new MO
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_a
|
||||
mo_form.product_qty = 1.0
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
mo.move_byproduct_ids.write({'product_id': self.product_c_id})
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.move_byproduct_ids.product_id, mo.move_byproduct_ids.move_line_ids.product_id)
|
||||
|
||||
def test_02_check_byproducts_update(self):
|
||||
"""
|
||||
Case 2: Update Product From Tracked Product to Non Tracked Product.
|
||||
"""
|
||||
self.bom_byproduct.byproduct_ids[0].product_id = self.produced_serial.id
|
||||
self.bom_byproduct.byproduct_ids[0].product_qty = 2
|
||||
mo = self.env["mrp.production"].create({
|
||||
'product_id': self.product_a.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': self.bom_byproduct.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
|
||||
mo.move_byproduct_ids.lot_ids = [(4, self.sn_1.id)]
|
||||
mo.move_byproduct_ids.lot_ids = [(4, self.sn_2.id)]
|
||||
|
||||
self.assertEqual(len(mo.move_byproduct_ids.move_line_ids), 2)
|
||||
|
||||
mo.move_byproduct_ids.write({'product_id': self.product_c_id})
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(len(mo.move_byproduct_ids.move_line_ids), 1)
|
||||
self.assertEqual(mo.move_byproduct_ids.product_id, mo.move_byproduct_ids.move_line_ids.product_id)
|
||||
|
||||
def test_03_check_byproducts_update(self):
|
||||
"""
|
||||
Case 3: Update Product From Non Tracked Product to Tracked Product.
|
||||
"""
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_a
|
||||
mo_form.product_qty = 2.0
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
mo.move_byproduct_ids.write({'product_id': self.produced_serial.id})
|
||||
|
||||
mo.move_byproduct_ids.lot_ids = [(4, self.sn_1.id)]
|
||||
mo.move_byproduct_ids.lot_ids = [(4, self.sn_2.id)]
|
||||
|
||||
self.assertFalse(mo.move_byproduct_ids.show_lots_text)
|
||||
self.assertTrue(mo.move_byproduct_ids.show_lots_m2o)
|
||||
self.assertFalse(mo.move_byproduct_ids.show_quant)
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(len(mo.move_byproduct_ids.move_line_ids), 2)
|
||||
self.assertEqual(mo.move_byproduct_ids.product_id, mo.move_byproduct_ids.move_line_ids.product_id)
|
||||
|
||||
def test_byproduct_qty_update(self):
|
||||
"""
|
||||
Test that byproduct quantity is updated to the quantity set on the Mo when the Mo is marked as done.ee
|
||||
"""
|
||||
self.bom_byproduct.byproduct_ids.product_qty = 0.0
|
||||
self.warehouse.manufacture_steps = 'pbm_sam'
|
||||
mo = self.env["mrp.production"].create({
|
||||
'product_id': self.product_a.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': self.bom_byproduct.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
mo.move_byproduct_ids.quantity = 1.0
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
picking = mo.picking_ids.filtered(lambda p: p.location_dest_id == self.warehouse.lot_stock_id)
|
||||
self.assertEqual(picking.state, 'assigned')
|
||||
byproduct_move = picking.move_ids.filtered(lambda m: m.product_id == self.bom_byproduct.byproduct_ids.product_id)
|
||||
self.assertEqual(byproduct_move.product_qty, 1.0)
|
||||
|
||||
def test_byproducts_bom_document(self):
|
||||
self.env.user.group_ids += self.env.ref('mrp.group_mrp_byproducts')
|
||||
doc_product_bom = self.env['product.document'].create({
|
||||
'name': 'doc_product_bom',
|
||||
'attached_on_mrp': 'bom',
|
||||
'res_id': self.product_a.id,
|
||||
'res_model': 'product.product',
|
||||
})
|
||||
|
||||
# ensures that the archived docs are not taken into account
|
||||
self.env['product.document'].create({
|
||||
'name': 'doc_product_bom_archived',
|
||||
'active': False,
|
||||
'attached_on_mrp': 'bom',
|
||||
'res_id': self.product_a.id,
|
||||
'res_model': 'product.product',
|
||||
})
|
||||
|
||||
doc_template_bom = self.env['product.document'].create({
|
||||
'name': 'doc_template_bom',
|
||||
'attached_on_mrp': 'bom',
|
||||
'res_id': self.product_a.product_tmpl_id.id,
|
||||
'res_model': 'product.template',
|
||||
})
|
||||
|
||||
attachments = doc_template_bom.ir_attachment_id + doc_product_bom.ir_attachment_id
|
||||
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.product_b.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_b.product_tmpl_id.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'byproduct_ids': [
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'product_qty': 1
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
# the two docs linked to the byproduct should be in the chatter
|
||||
self.assertEqual(bom._get_extra_attachments(), attachments)
|
||||
|
||||
def test_3_steps_byproduct(self):
|
||||
""" Test that non-bom byproducts are correctly pushed from
|
||||
post-production to the stock location in 3-steps manufacture. """
|
||||
self.warehouse.manufacture_steps = 'pbm_sam'
|
||||
self.env.user.groups_id += self.env.ref('mrp.group_mrp_byproducts')
|
||||
self.env.user.group_ids += self.env.ref('mrp.group_mrp_byproducts')
|
||||
component, final_product, byproduct = self.env['product.product'].create([{
|
||||
'name': name,
|
||||
'type': 'product'
|
||||
'is_storable': True,
|
||||
} for name in ['Old Blood', 'Insight', 'Eyes on the Inside']])
|
||||
self.env['stock.quant']._update_available_quantity(component, self.warehouse.lot_stock_id, 1)
|
||||
mo = self.env["mrp.production"].create({
|
||||
|
|
@ -425,18 +572,41 @@ class TestMrpByProduct(common.TransactionCase):
|
|||
mo_form = Form(mo)
|
||||
with mo_form.move_raw_ids.new() as line:
|
||||
line.product_id = component
|
||||
line.product_uom_qty = 1
|
||||
with mo_form.move_byproduct_ids.new() as line:
|
||||
line.product_id = byproduct
|
||||
line.product_uom_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
preprod_picking = mo.picking_ids.filtered(lambda p: p.state == 'assigned')
|
||||
preprod_picking.move_line_ids.qty_done = 1
|
||||
preprod_picking.button_validate()
|
||||
mo.move_raw_ids.quantity_done = 1
|
||||
mo.qty_producing = 1
|
||||
mo.button_mark_done()
|
||||
postprod_picking = mo.picking_ids.filtered(lambda p: p.state == 'assigned')
|
||||
|
||||
self.assertEqual(len(postprod_picking.move_ids), 2)
|
||||
self.assertEqual(postprod_picking.move_ids.product_id, final_product + byproduct)
|
||||
self.assertEqual(postprod_picking.location_dest_id, self.warehouse.lot_stock_id)
|
||||
|
||||
def test_over_produce_by_products_with_cost_share(self):
|
||||
"""
|
||||
Tests that overproducing by-products with a set cost share
|
||||
behaves as expected (as it should rely on the merge move) for
|
||||
the extra move.
|
||||
"""
|
||||
# Create new MO
|
||||
self.env.user.group_ids = [Command.link(self.ref('mrp.group_mrp_byproducts'))]
|
||||
self.bom_byproduct.byproduct_ids.cost_share = 3.3
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': self.product_a.id,
|
||||
'product_qty': 1.0,
|
||||
})
|
||||
mo.action_confirm()
|
||||
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 1.0
|
||||
with mo_form.move_byproduct_ids.edit(0) as by_product_move:
|
||||
by_product_move.quantity = 10.0
|
||||
mo.button_mark_done()
|
||||
self.assertRecordValues(mo.move_byproduct_ids, [
|
||||
{'quantity': 10.0, 'state': 'done'},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -116,3 +116,25 @@ class TestMrpCancelMO(TestMrpCommon):
|
|||
|
||||
self.assertEqual(mo.move_finished_ids.state, 'cancel')
|
||||
self.assertEqual(mo.state, 'cancel')
|
||||
|
||||
def test_cannot_cancel_done_mo_with_three_steps(self):
|
||||
"""Test that a done manufacturing order cannot be canceled.
|
||||
|
||||
The test ensures that when the warehouse uses a 3-step manufacturing route (Pick → Produce → Store),
|
||||
attempting to cancel a manufacturing order that is already in 'done' state raises a UserError.
|
||||
It also verifies that the linked pickings are not canceled in this case.
|
||||
"""
|
||||
# Enable 3-step manufacturing process
|
||||
self.warehouse_1.manufacture_steps = 'pbm_sam'
|
||||
# Create and confirm a manufacturing order
|
||||
mo = self.env['mrp.production'].create({
|
||||
'bom_id': self.bom_1.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
with self.assertRaises(UserError):
|
||||
mo.action_cancel()
|
||||
self.assertNotEqual(mo.picking_ids.mapped('state'), ['cancel', 'cancel'])
|
||||
with self.assertRaises(UserError):
|
||||
mo.unlink()
|
||||
|
|
|
|||
473
odoo-bringout-oca-ocb-mrp/mrp/tests/test_consume_component.py
Normal file
473
odoo-bringout-oca-ocb-mrp/mrp/tests/test_consume_component.py
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
import copy
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import common, tagged, Form
|
||||
|
||||
|
||||
class TestConsumeComponentCommon(common.TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
The following variables are used in each test to define the number of MO to generate.
|
||||
They're also used as a verification in the executeConsumptionTriggers() to see if enough MO were passed to it
|
||||
in order to test all the triggers.
|
||||
|
||||
SERIAL : MO's product_tracking is 'serial'
|
||||
DEFAULT : MO's product_tracking is 'none' or 'lot'
|
||||
AVAILABLE : MO'S raw components are fully available
|
||||
"""
|
||||
super().setUpClass()
|
||||
|
||||
cls.SERIAL_AVAILABLE_TRIGGERS_COUNT = 3
|
||||
cls.DEFAULT_AVAILABLE_TRIGGERS_COUNT = 2
|
||||
cls.SERIAL_TRIGGERS_COUNT = 2
|
||||
cls.DEFAULT_TRIGGERS_COUNT = 1
|
||||
|
||||
cls.manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture')
|
||||
cls.stock_id = cls.env.ref('stock.stock_location_stock').id
|
||||
|
||||
cls.picking_type = cls.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
|
||||
cls.picking_type.use_create_components_lots = True
|
||||
|
||||
# Create Products & Components
|
||||
cls.produced_lot = cls.env['product.product'].create({
|
||||
'name': 'Produced Lot',
|
||||
'is_storable': True,
|
||||
'tracking': 'lot',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
cls.produced_serial = cls.env['product.product'].create({
|
||||
'name': 'Produced Serial',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
cls.produced_none = cls.env['product.product'].create({
|
||||
'name': 'Produced None',
|
||||
'is_storable': True,
|
||||
'tracking': 'none',
|
||||
'route_ids': [(4, cls.manufacture_route.id, 0)],
|
||||
})
|
||||
|
||||
cls.raw_lot = cls.env['product.product'].create({
|
||||
'name': 'Raw Lot',
|
||||
'is_storable': True,
|
||||
'tracking': 'lot',
|
||||
})
|
||||
cls.raw_serial = cls.env['product.product'].create({
|
||||
'name': 'Raw Serial',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
cls.raw_none = cls.env['product.product'].create({
|
||||
'name': 'Raw None',
|
||||
'is_storable': True,
|
||||
'tracking': 'none',
|
||||
})
|
||||
|
||||
cls.raws = [cls.raw_none, cls.raw_lot, cls.raw_serial]
|
||||
|
||||
# Workcenter
|
||||
cls.workcenter = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line',
|
||||
})
|
||||
|
||||
# BoMs
|
||||
cls.bom_none = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.produced_none.product_tmpl_id.id,
|
||||
'product_uom_id': cls.produced_none.uom_id.id,
|
||||
'consumption': 'flexible',
|
||||
'sequence': 1
|
||||
})
|
||||
|
||||
cls.bom_none_lines = cls.create_bom_lines(cls.bom_none, cls.raws, [3, 2, 1])
|
||||
|
||||
cls.bom_lot = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.produced_lot.product_tmpl_id.id,
|
||||
'product_uom_id': cls.produced_lot.uom_id.id,
|
||||
'consumption': 'flexible',
|
||||
'sequence': 2
|
||||
})
|
||||
|
||||
cls.bom_lot_lines = cls.create_bom_lines(cls.bom_lot, cls.raws, [3, 2, 1])
|
||||
|
||||
cls.bom_serial = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.produced_serial.product_tmpl_id.id,
|
||||
'product_uom_id': cls.produced_serial.uom_id.id,
|
||||
'consumption': 'flexible',
|
||||
'sequence': 1
|
||||
})
|
||||
|
||||
cls.bom_serial_lines = cls.create_bom_lines(cls.bom_serial, cls.raws, [3, 2, 1])
|
||||
|
||||
# Manufacturing Orders
|
||||
cls.mo_none_tmpl = {
|
||||
'product_id': cls.produced_none.id,
|
||||
'product_uom_id': cls.produced_none.uom_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': cls.bom_none.id
|
||||
}
|
||||
|
||||
cls.mo_lot_tmpl = {
|
||||
'product_id': cls.produced_lot.id,
|
||||
'product_uom_id': cls.produced_lot.uom_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': cls.bom_lot.id
|
||||
}
|
||||
|
||||
cls.mo_serial_tmpl = {
|
||||
'product_id': cls.produced_serial.id,
|
||||
'product_uom_id': cls.produced_serial.uom_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': cls.bom_serial.id
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_quant(cls, product, qty, offset=0, name="L"):
|
||||
i = 1
|
||||
if product.tracking == 'serial':
|
||||
i, qty = qty, 1
|
||||
if name == "L":
|
||||
name = "S"
|
||||
|
||||
vals = []
|
||||
for x in range(1, i + 1):
|
||||
qDict = {
|
||||
'location_id': cls.stock_id,
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': qty,
|
||||
}
|
||||
|
||||
if product.tracking != 'none':
|
||||
qDict['lot_id'] = cls.env['stock.lot'].create({
|
||||
'name': name + str(offset + x),
|
||||
'product_id': product.id,
|
||||
}).id
|
||||
vals.append(qDict)
|
||||
|
||||
return cls.env['stock.quant'].create(vals)
|
||||
|
||||
@classmethod
|
||||
def create_bom_lines(cls, bom, products, quantities=None):
|
||||
if quantities is None:
|
||||
quantities = [1 for i in range(len(products))]
|
||||
|
||||
vals = []
|
||||
for product, seq in zip(products, range(len(products))):
|
||||
vals.append({
|
||||
'product_id': product.id,
|
||||
'product_qty': quantities[seq],
|
||||
'product_uom_id': product.uom_id.id,
|
||||
'sequence': seq,
|
||||
'bom_id': bom.id,
|
||||
})
|
||||
|
||||
return cls.env['mrp.bom.line'].create(vals)
|
||||
|
||||
@classmethod
|
||||
def create_mo(cls, template, count):
|
||||
vals = []
|
||||
for _ in range(count):
|
||||
vals.append(copy.deepcopy(template))
|
||||
mos = cls.env['mrp.production'].create(vals)
|
||||
return mos
|
||||
|
||||
def executeConsumptionTriggers(self, mrp_productions):
|
||||
"""There's 3 different triggers to test : _onchange_producing(), action_generate_serial(), button_mark_done().
|
||||
|
||||
Depending on the tracking of the final product and the availability of the components,
|
||||
only a part of these 3 triggers is available or intended to work.
|
||||
|
||||
This function automatically call and process the appropriate triggers.
|
||||
"""
|
||||
tracking = mrp_productions[0].product_tracking
|
||||
sameTracking = True
|
||||
for mo in mrp_productions:
|
||||
sameTracking = sameTracking and mo.product_tracking == tracking
|
||||
self.assertTrue(sameTracking, "MOs passed to the executeConsumptionTriggers method shall have the same product_tracking")
|
||||
|
||||
isSerial = tracking == 'serial'
|
||||
isAvailable = all(move.state == 'assigned' for move in mrp_productions.move_raw_ids)
|
||||
isComponentTracking = any(move.has_tracking != 'none' for move in mrp_productions.move_raw_ids)
|
||||
|
||||
countOk = True
|
||||
length = len(mrp_productions)
|
||||
if isSerial:
|
||||
if isAvailable:
|
||||
countOk = length == self.SERIAL_AVAILABLE_TRIGGERS_COUNT
|
||||
else:
|
||||
countOk = length == self.SERIAL_TRIGGERS_COUNT
|
||||
else:
|
||||
if isAvailable:
|
||||
countOk = length == self.DEFAULT_AVAILABLE_TRIGGERS_COUNT
|
||||
else:
|
||||
countOk = length == self.DEFAULT_TRIGGERS_COUNT
|
||||
self.assertTrue(countOk, "The number of MOs passed to the executeConsumptionTriggers method does not match the associated TRIGGERS_COUNT")
|
||||
|
||||
mrp_productions[0].qty_producing = mrp_productions[0].product_qty
|
||||
mrp_productions[0]._onchange_qty_producing()
|
||||
|
||||
i = 1
|
||||
if isSerial:
|
||||
mrp_productions[i].action_generate_serial()
|
||||
i += 1
|
||||
|
||||
if isAvailable:
|
||||
error = False
|
||||
try:
|
||||
mrp_productions[i].button_mark_done()
|
||||
except UserError:
|
||||
error = True
|
||||
|
||||
self.assertFalse(error, "Immediate Production shall not raise an error.")
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestConsumeComponent(TestConsumeComponentCommon):
|
||||
def test_option_enabled_and_qty_available(self):
|
||||
"""Option enabled, qty available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are fully consumed
|
||||
"""
|
||||
|
||||
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
|
||||
mo_all = mo_none + mo_serial + mo_lot
|
||||
mo_all.action_confirm()
|
||||
|
||||
all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT
|
||||
|
||||
quant = self.create_quant(self.raw_none, 3*all_qty)
|
||||
quant |= self.create_quant(self.raw_lot, 2*all_qty)
|
||||
quant |= self.create_quant(self.raw_serial, 1*all_qty)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
# Quantities are fully reserved (stock.move state is available)
|
||||
mo_all.action_assign()
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(mov.product_qty, mov.quantity, "Reserved quantity shall be equal to To Consume quantity.")
|
||||
|
||||
self.executeConsumptionTriggers(mo_serial)
|
||||
self.executeConsumptionTriggers(mo_none)
|
||||
self.executeConsumptionTriggers(mo_lot)
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertTrue(mov.picked, "All components should be picked")
|
||||
|
||||
def test_option_enabled_and_qty_not_available(self):
|
||||
"""Option enabled, qty not available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are not consumed
|
||||
"""
|
||||
|
||||
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_TRIGGERS_COUNT)
|
||||
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_TRIGGERS_COUNT)
|
||||
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_TRIGGERS_COUNT)
|
||||
|
||||
mo_all = mo_none + mo_serial + mo_lot
|
||||
mo_all.action_confirm()
|
||||
|
||||
# Quantities are not reserved at all (stock.move state is confirmed)
|
||||
mo_all.action_assign()
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(0, mov.quantity, "Reserved quantity shall be equal to 0.")
|
||||
|
||||
self.executeConsumptionTriggers(mo_serial)
|
||||
self.executeConsumptionTriggers(mo_none)
|
||||
self.executeConsumptionTriggers(mo_lot)
|
||||
|
||||
for mov in mo_all.move_raw_ids:
|
||||
if mov.has_tracking == 'none':
|
||||
self.assertTrue(mov.picked, "components should be picked even without no quantity reserved")
|
||||
else:
|
||||
self.assertEqual(mov.product_qty, mov.quantity, "Done quantity shall be equal to To Consume quantity.")
|
||||
|
||||
def test_option_enabled_and_qty_partially_available(self):
|
||||
"""Option enabled, qty partially available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are partially consumed
|
||||
"""
|
||||
|
||||
# Update BoM serial component qty
|
||||
self.bom_none_lines[2].product_qty = 2
|
||||
self.bom_serial_lines[2].product_qty = 2
|
||||
self.bom_lot_lines[2].product_qty = 2
|
||||
|
||||
raw_none_qty = 2
|
||||
raw_tracked_qty = 1
|
||||
|
||||
quant = self.create_quant(self.raw_none, raw_none_qty)
|
||||
quant |= self.create_quant(self.raw_lot, raw_tracked_qty)
|
||||
quant |= self.create_quant(self.raw_serial, raw_tracked_qty)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
# We must create & process each MO at once as we must assign quants for each individually
|
||||
def testUnit(mo_tmpl, serialTrigger=None):
|
||||
mo = self.create_mo(mo_tmpl, 1)
|
||||
mo.action_confirm()
|
||||
|
||||
# are partially reserved (stock.move state is partially_available)
|
||||
mo.action_assign()
|
||||
for mov in mo.move_raw_ids:
|
||||
if mov.has_tracking == "none":
|
||||
self.assertEqual(raw_none_qty, mov.quantity, "Reserved quantity shall be equal to " + str(raw_none_qty) + ".")
|
||||
else:
|
||||
self.assertEqual(raw_tracked_qty, mov.quantity, "Reserved quantity shall be equal to " + str(raw_tracked_qty) + ".")
|
||||
|
||||
if serialTrigger is None:
|
||||
self.executeConsumptionTriggers(mo)
|
||||
elif serialTrigger == 1:
|
||||
mo.qty_producing = 1
|
||||
mo._set_qty_producing(False)
|
||||
elif serialTrigger == 2:
|
||||
mo.action_generate_serial()
|
||||
|
||||
for mov in mo.move_raw_ids:
|
||||
if mov.has_tracking == "none":
|
||||
self.assertTrue(mov.picked, "non tracked components should be picked")
|
||||
else:
|
||||
self.assertEqual(mov.product_qty, mov.quantity, "Done quantity shall be equal to To Consume quantity.")
|
||||
mo.action_cancel()
|
||||
|
||||
testUnit(self.mo_none_tmpl)
|
||||
testUnit(self.mo_lot_tmpl)
|
||||
testUnit(self.mo_serial_tmpl, 1)
|
||||
testUnit(self.mo_serial_tmpl, 2)
|
||||
|
||||
def test_tracked_production_2_steps_manufacturing(self):
|
||||
"""
|
||||
Create an MO for a product tracked by SN in 2-steps manufacturing with tracked components.
|
||||
Assign a SN to the final product using the auto generation, then validate the pbm picking.
|
||||
This test checks that the tracking of components is updated on the MO.
|
||||
"""
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
warehouse.manufacture_steps = 'pbm'
|
||||
bom = self.bom_serial
|
||||
bom.product_id = self.produced_serial
|
||||
components = self.bom_serial.bom_line_ids.mapped('product_id')
|
||||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_1',
|
||||
'product_id': components[1].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_2 = self.env['stock.lot'].create({
|
||||
'name': 'SN01',
|
||||
'product_id': components[2].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(components[0], self.env.ref('stock.warehouse0').lot_stock_id, 3)
|
||||
self.env['stock.quant']._update_available_quantity(components[1], self.env.ref('stock.warehouse0').lot_stock_id, 2, lot_id=lot_1)
|
||||
self.env['stock.quant']._update_available_quantity(components[2], self.env.ref('stock.warehouse0').lot_stock_id, 1, lot_id=lot_2)
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': bom.product_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': bom.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
self.assertRecordValues(mo.picking_ids.move_ids, [
|
||||
{'quantity': 3.0, 'picked': False, 'lot_ids': []},
|
||||
{'quantity': 2.0, 'picked': False, 'lot_ids': lot_1.ids},
|
||||
{'quantity': 1.0, 'picked': False, 'lot_ids': lot_2.ids},
|
||||
])
|
||||
mo.action_generate_serial()
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{'should_consume_qty': 3.0, 'quantity': 3.0, 'picked': True, 'lot_ids': []},
|
||||
{'should_consume_qty': 2.0, 'quantity': 0.0, 'picked': False, 'lot_ids': []},
|
||||
{'should_consume_qty': 1.0, 'quantity': 0.0, 'picked': False, 'lot_ids': []},
|
||||
])
|
||||
self.assertTrue(mo.lot_producing_ids)
|
||||
mo.picking_ids.button_validate()
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{'quantity': 3.0, 'picked': True, 'lot_ids': []},
|
||||
{'quantity': 2.0, 'picked': False, 'lot_ids': lot_1.ids},
|
||||
{'quantity': 1.0, 'picked': False, 'lot_ids': lot_2.ids},
|
||||
])
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
|
||||
def test_automatic_consume_new_added_component(self):
|
||||
"""
|
||||
Create an MO for a product and set qty_producing than add a new component with quantity and automatically it's picked.
|
||||
"""
|
||||
sfg_product, compo1, compo2 = self.env['product.product'].create([
|
||||
{
|
||||
'name': 'SFG Product',
|
||||
'is_storable': True,
|
||||
'route_ids': [(4, self.manufacture_route.id, 0)],
|
||||
},
|
||||
{
|
||||
'name': 'Compo 1',
|
||||
'is_storable': True,
|
||||
},
|
||||
{
|
||||
'name': 'Compo 2',
|
||||
'is_storable': True,
|
||||
}
|
||||
])
|
||||
|
||||
quant = self.create_quant(compo1, 10)
|
||||
quant |= self.create_quant(compo2, 10)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': sfg_product.product_tmpl_id.id,
|
||||
'product_uom_id': sfg_product.uom_id.id,
|
||||
'consumption': 'flexible',
|
||||
'sequence': 1
|
||||
})
|
||||
self.create_bom_lines(bom, compo1, [1])
|
||||
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': sfg_product.id,
|
||||
'product_uom_id': sfg_product.uom_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': bom.id
|
||||
})
|
||||
mo.action_confirm()
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{'product_uom_qty': 1.0, 'picked': False},
|
||||
])
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 1.0
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{'should_consume_qty': 1.0, 'quantity': 1.0, 'picked': True},
|
||||
])
|
||||
move = self.env['stock.move'].create({
|
||||
'product_id': compo2.id,
|
||||
'raw_material_production_id': mo.id,
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.env.company.id)]).id,
|
||||
})
|
||||
move.should_consume_qty = 1
|
||||
move.quantity = 1
|
||||
move._action_assign()
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{'should_consume_qty': 1.0, 'quantity': 1.0, 'picked': True},
|
||||
{'should_consume_qty': 1.0, 'quantity': 1.0, 'picked': True},
|
||||
])
|
||||
|
||||
def test_no_component_consumption_on_lot_removal(self):
|
||||
"""
|
||||
If we have a manufacturing order (MO) for a product tracked by lot, and we assign a lot number
|
||||
and then unassign it, the components should not be consumed at that point.
|
||||
"""
|
||||
quant = self.create_quant(self.raw_none, 3)
|
||||
quant |= self.create_quant(self.raw_lot, 2)
|
||||
quant |= self.create_quant(self.raw_serial, 1)
|
||||
quant.action_apply_inventory()
|
||||
mo = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_TRIGGERS_COUNT)
|
||||
mo.action_confirm()
|
||||
mo.action_generate_serial()
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{'quantity': 3.0, 'picked': False},
|
||||
{'quantity': 2.0, 'picked': False},
|
||||
{'quantity': 1.0, 'picked': False},
|
||||
])
|
||||
mo.action_clear_lot_producing_ids()
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{'quantity': 0.0, 'picked': False},
|
||||
{'quantity': 0.0, 'picked': False},
|
||||
{'quantity': 0.0, 'picked': False},
|
||||
])
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
from odoo.tests import tagged
|
||||
from odoo.addons.mrp.tests.common_consume_tracked_component import TestConsumeTrackedComponentCommon
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestConsumeTrackedComponent(TestConsumeTrackedComponentCommon):
|
||||
|
||||
def test_option_disabled_and_qty_available(self):
|
||||
"""
|
||||
Option disabled, qty available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are only consumed on button_mark_done trigger
|
||||
"""
|
||||
|
||||
self.picking_type.use_auto_consume_components_lots = False
|
||||
|
||||
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
|
||||
mo_all = mo_none + mo_serial + mo_lot
|
||||
mo_all.action_confirm()
|
||||
|
||||
all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT
|
||||
|
||||
quant = self.create_quant(self.raw_none, 3*all_qty)
|
||||
quant |= self.create_quant(self.raw_lot, 2*all_qty)
|
||||
quant |= self.create_quant(self.raw_serial, 1*all_qty)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
#Quantities are fully reserved (stock.move state is available)
|
||||
mo_all.action_assign()
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.")
|
||||
|
||||
#Test for Serial Product
|
||||
self.executeConsumptionTriggers(mo_serial)
|
||||
self.executeConsumptionTriggers(mo_none)
|
||||
self.executeConsumptionTriggers(mo_lot)
|
||||
for mov in mo_all.move_raw_ids:
|
||||
if mov.has_tracking == 'none' or mov.raw_material_production_id.state == 'done':
|
||||
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
else:
|
||||
self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to 0.")
|
||||
|
||||
def test_option_enabled_and_qty_available(self):
|
||||
"""
|
||||
Option enabled, qty available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are fully consumed
|
||||
"""
|
||||
|
||||
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT)
|
||||
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT)
|
||||
|
||||
mo_all = mo_none + mo_serial + mo_lot
|
||||
mo_all.action_confirm()
|
||||
|
||||
all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT
|
||||
|
||||
quant = self.create_quant(self.raw_none, 3*all_qty)
|
||||
quant |= self.create_quant(self.raw_lot, 2*all_qty)
|
||||
quant |= self.create_quant(self.raw_serial, 1*all_qty)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
#Quantities are fully reserved (stock.move state is available)
|
||||
mo_all.action_assign()
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.")
|
||||
|
||||
self.executeConsumptionTriggers(mo_serial)
|
||||
self.executeConsumptionTriggers(mo_none)
|
||||
self.executeConsumptionTriggers(mo_lot)
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
|
||||
def test_option_enabled_and_qty_not_available(self):
|
||||
"""
|
||||
Option enabled, qty not available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are not consumed
|
||||
"""
|
||||
|
||||
mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_TRIGGERS_COUNT)
|
||||
mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_TRIGGERS_COUNT)
|
||||
mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_TRIGGERS_COUNT)
|
||||
|
||||
mo_all = mo_none + mo_serial + mo_lot
|
||||
mo_all.action_confirm()
|
||||
|
||||
#Quantities are not reserved at all (stock.move state is confirmed)
|
||||
mo_all.action_assign()
|
||||
for mov in mo_all.move_raw_ids:
|
||||
self.assertEqual(0, mov.reserved_availability, "Reserved quantity shall be equal to 0.")
|
||||
|
||||
self.executeConsumptionTriggers(mo_serial)
|
||||
self.executeConsumptionTriggers(mo_none)
|
||||
self.executeConsumptionTriggers(mo_lot)
|
||||
|
||||
for mov in mo_all.move_raw_ids:
|
||||
if mov.has_tracking == 'none':
|
||||
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
else:
|
||||
self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
|
||||
def test_option_enabled_and_qty_partially_available(self):
|
||||
"""
|
||||
Option enabled, qty partially available
|
||||
-> Not Tracked components are fully consumed
|
||||
-> Tracked components are partially consumed
|
||||
"""
|
||||
|
||||
#update BoM serial component qty
|
||||
self.bom_none_lines[2].product_qty = 2
|
||||
self.bom_serial_lines[2].product_qty = 2
|
||||
self.bom_lot_lines[2].product_qty = 2
|
||||
|
||||
raw_none_qty = 2
|
||||
raw_tracked_qty = 1
|
||||
|
||||
quant = self.create_quant(self.raw_none, raw_none_qty)
|
||||
quant |= self.create_quant(self.raw_lot, raw_tracked_qty)
|
||||
quant |= self.create_quant(self.raw_serial, raw_tracked_qty)
|
||||
quant.action_apply_inventory()
|
||||
|
||||
#We must create & process each MO at once as we must assign quants for each individually
|
||||
def testUnit(mo_tmpl, serialTrigger=None):
|
||||
mo = self.create_mo(mo_tmpl, 1)
|
||||
mo.action_confirm()
|
||||
|
||||
#Quantities are partially reserved (stock.move state is partially_available)
|
||||
mo.action_assign()
|
||||
for mov in mo.move_raw_ids:
|
||||
if mov.has_tracking == "none":
|
||||
self.assertEqual(raw_none_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_none_qty)+ ".")
|
||||
else:
|
||||
self.assertEqual(raw_tracked_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_tracked_qty)+ ".")
|
||||
|
||||
if serialTrigger is None:
|
||||
self.executeConsumptionTriggers(mo)
|
||||
elif serialTrigger == 1:
|
||||
mo.qty_producing = mo.product_qty
|
||||
mo._onchange_producing()
|
||||
elif serialTrigger == 2:
|
||||
mo.action_generate_serial()
|
||||
|
||||
for mov in mo.move_raw_ids:
|
||||
if mov.has_tracking == "none":
|
||||
self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.")
|
||||
else:
|
||||
self.assertEqual(raw_tracked_qty, mov.quantity_done, "Done quantity shall be equal to " + str(raw_tracked_qty)+ ".")
|
||||
mo.action_cancel()
|
||||
|
||||
testUnit(self.mo_none_tmpl)
|
||||
testUnit(self.mo_lot_tmpl)
|
||||
testUnit(self.mo_serial_tmpl, 1)
|
||||
testUnit(self.mo_serial_tmpl, 2)
|
||||
|
|
@ -1,37 +1,28 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import tagged, Form, HttpCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTourManualConsumption(HttpCase):
|
||||
def test_mrp_manual_consumption(self):
|
||||
"""Test manual consumption mechanism. Test when manual consumption is
|
||||
True, quantity_done won't be updated automatically. Bom line with tracked
|
||||
products or operations should be set to manual consumption automatically.
|
||||
Also test that when manually change quantity_done, manual consumption
|
||||
will be set to True. Also test when create backorder, the manual consumption
|
||||
should be set according to the bom.
|
||||
def test_mrp_manual_consumption_02(self):
|
||||
"""
|
||||
test that when a new quantity is manually set for a component,
|
||||
and the field picked is set to True,
|
||||
and the MO is marked as done, the component quantity is not overwritten.
|
||||
"""
|
||||
Product = self.env['product.product']
|
||||
product_finish = Product.create({
|
||||
'name': 'finish',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'none',})
|
||||
product_nt = Product.create({
|
||||
'name': 'No tracking',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'none',})
|
||||
product_sn = Product.create({
|
||||
'name': 'Serial',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',})
|
||||
product_lot = Product.create({
|
||||
'name': 'Lot',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': product_finish.id,
|
||||
'product_tmpl_id': product_finish.product_tmpl_id.id,
|
||||
|
|
@ -39,8 +30,6 @@ class TestTourManualConsumption(HttpCase):
|
|||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_nt.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': product_sn.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': product_lot.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
|
|
@ -50,141 +39,318 @@ class TestTourManualConsumption(HttpCase):
|
|||
mo_form.product_qty = 10
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
|
||||
# test no updating
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5
|
||||
mo = mo_form.save()
|
||||
move_nt, move_sn, move_lot = mo.move_raw_ids
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
move_nt = mo.move_raw_ids
|
||||
self.assertEqual(move_nt.manual_consumption, False)
|
||||
self.assertEqual(move_nt.quantity_done, 5)
|
||||
self.assertEqual(move_sn.manual_consumption, True)
|
||||
self.assertEqual(move_sn.quantity_done, 0)
|
||||
self.assertEqual(move_lot.manual_consumption, True)
|
||||
self.assertEqual(move_lot.quantity_done, 0)
|
||||
self.assertEqual(move_nt.quantity, 0)
|
||||
self.assertFalse(move_nt.picked)
|
||||
|
||||
action_id = self.env.ref('mrp.menu_mrp_production_action').action
|
||||
url = "/web#model=mrp.production&view_type=form&action=%s&id=%s" % (str(action_id.id), str(mo.id))
|
||||
self.start_tour(url, "test_mrp_manual_consumption", login="admin", timeout=200)
|
||||
url = f"/odoo/action-mrp.mrp_production_action/{mo.id}"
|
||||
self.start_tour(url, "test_mrp_manual_consumption_02", login="admin", timeout=100)
|
||||
|
||||
self.assertEqual(move_nt.manual_consumption, True)
|
||||
self.assertEqual(move_nt.quantity_done, 6.0)
|
||||
self.assertEqual(move_sn.manual_consumption, True)
|
||||
self.assertEqual(move_sn.quantity_done, 0)
|
||||
self.assertEqual(move_lot.manual_consumption, True)
|
||||
self.assertEqual(move_lot.quantity_done, 0)
|
||||
|
||||
backorder = mo.procurement_group_id.mrp_production_ids - mo
|
||||
move_nt = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_nt)
|
||||
move_sn = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_sn)
|
||||
move_lot = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_lot)
|
||||
self.assertEqual(move_nt.manual_consumption, False)
|
||||
self.assertEqual(move_sn.manual_consumption, True)
|
||||
self.assertEqual(move_lot.manual_consumption, True)
|
||||
|
||||
self.assertEqual(move_nt.picked, True)
|
||||
self.assertEqual(move_nt.quantity, 16.0)
|
||||
|
||||
class TestManualConsumption(TestMrpCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
|
||||
|
||||
def test_manual_consumption_backorder_00(self):
|
||||
"""Test when use_auto_consume_components_lots is not set, manual consumption
|
||||
of the backorder is correctly set.
|
||||
"""
|
||||
mo, _, _final, c1, c2 = self.generate_mo('none', 'lot', 'none', qty_final=2)
|
||||
|
||||
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot',
|
||||
'product_id': c1.id,
|
||||
'company_id': self.env.company.id,
|
||||
cls.env.ref('base.group_user').write({
|
||||
'implied_ids': [Command.link(cls.env.ref('stock.group_production_lot').id)],
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(c1, self.stock_location, 8, lot_id=lot)
|
||||
self.env['stock.quant']._update_available_quantity(c2, self.stock_location, 2)
|
||||
|
||||
mo.action_assign()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.save()
|
||||
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c1.id).mapped("quantity_done")), 0)
|
||||
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c2.id).mapped("quantity_done")), 1)
|
||||
|
||||
details_operation_form = Form(mo.move_raw_ids.filtered(lambda m: m.product_id == c1), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 4
|
||||
ml.lot_id = lot
|
||||
details_operation_form.save()
|
||||
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
backorder_mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
self.assertTrue(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
|
||||
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
|
||||
|
||||
def test_manual_consumption_backorder_01(self):
|
||||
"""Test when use_auto_consume_components_lots is set, manual consumption
|
||||
of the backorder is correctly set.
|
||||
def test_manual_consumption_with_different_component_price(self):
|
||||
"""
|
||||
picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
|
||||
picking_type.use_auto_consume_components_lots = True
|
||||
|
||||
mo, _, _final, c1, c2 = self.generate_mo('none', 'lot', 'none', qty_final=2)
|
||||
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot',
|
||||
'product_id': c1.id,
|
||||
'company_id': self.env.company.id,
|
||||
Test that the moves are merged correctly, even if the products have been used with different prices:
|
||||
- Create a product with a price of $10 and use it in a BoM with 1 unit.
|
||||
- Create a MO with this BoM and confirm it.
|
||||
- Update the price of the component to $20 and adjust the consumed quantity to 2.
|
||||
- Mark the MO as done.
|
||||
- Another move should be created and merged with the first move.
|
||||
"""
|
||||
self.bom_4.consumption = 'warning'
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'is_storable': True,
|
||||
'standard_price': 10,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(c1, self.stock_location, 8, lot_id=lot)
|
||||
self.env['stock.quant']._update_available_quantity(c2, self.stock_location, 2)
|
||||
|
||||
mo.action_assign()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.save()
|
||||
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c1.id).mapped("quantity_done")), 4)
|
||||
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id.id == c2.id).mapped("quantity_done")), 1)
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 2)
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_qty': 1,
|
||||
'bom_id': self.bom_4.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
component.standard_price = 20
|
||||
mo.move_raw_ids.quantity = 2.0
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.move_raw_ids.manual_consumption = True
|
||||
self.assertEqual(mo.state, 'progress')
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
backorder_mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
consumption_warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context']))
|
||||
action = consumption_warning.save().action_confirm()
|
||||
self.assertEqual(len(mo.move_raw_ids), 1)
|
||||
self.assertEqual(mo.move_raw_ids.quantity, 2)
|
||||
|
||||
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c1).manual_consumption)
|
||||
self.assertFalse(backorder_mo.move_raw_ids.filtered(lambda m: m.product_id == c2).manual_consumption)
|
||||
|
||||
def test_manual_consumption_split_merge_00(self):
|
||||
"""Test manual consumption is correctly set after split or merge.
|
||||
def test_manual_consumption_quantity_change(self):
|
||||
"""Test manual consumption mechanism.
|
||||
1. Test when a move is manual consumption but NOT picked, quantity will be updated automatically.
|
||||
2. Test when a move is manual consumption but IS picked, quantity will not be updated automatically.
|
||||
3. Test when create backorder, the manual consumption should be set according to the bom.
|
||||
"""
|
||||
# Change 'Units' rounding to 1 (integer only quantities)
|
||||
self.uom_unit.rounding = 1
|
||||
# Create a mo for 10 products
|
||||
mo, _, _, p1, p2 = self.generate_mo('none', 'lot', 'none', qty_final=10)
|
||||
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)
|
||||
Product = self.env['product.product']
|
||||
product_finish = Product.create({
|
||||
'name': 'finish',
|
||||
'is_storable': True,
|
||||
'tracking': 'none'})
|
||||
product_auto_consumption = Product.create({
|
||||
'name': 'Automatic',
|
||||
'is_storable': True,
|
||||
'tracking': 'none'})
|
||||
product_manual_consumption = Product.create({
|
||||
'name': 'Manual',
|
||||
'is_storable': True,
|
||||
'tracking': 'none'})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': product_finish.id,
|
||||
'product_tmpl_id': product_finish.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_auto_consumption.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': product_manual_consumption.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
# Split in 3 parts
|
||||
action = mo.action_split()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
wizard.counter = 3
|
||||
action = wizard.save().action_split()
|
||||
for production in mo.procurement_group_id.mrp_production_ids:
|
||||
self.assertTrue(production.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
|
||||
self.assertFalse(production.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)
|
||||
def get_moves(mo):
|
||||
move_auto = mo.move_raw_ids.filtered(lambda m: m.product_id == product_auto_consumption)
|
||||
move_manual = mo.move_raw_ids.filtered(lambda m: m.product_id == product_manual_consumption)
|
||||
return move_auto, move_manual
|
||||
|
||||
# Merge them back
|
||||
action = mo.procurement_group_id.mrp_production_ids.action_merge()
|
||||
mo = self.env[action['res_model']].browse(action['res_id'])
|
||||
self.assertTrue(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).manual_consumption)
|
||||
self.assertFalse(mo.move_raw_ids.filtered(lambda m: m.product_id == p2).manual_consumption)
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_finish
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 10
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
|
||||
# After updating qty_producing, quantity changes for both moves, but manual move will remain not picked
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5
|
||||
mo = mo_form.save()
|
||||
move_auto, move_manual = get_moves(mo)
|
||||
self.assertEqual(move_auto.manual_consumption, False)
|
||||
self.assertEqual(move_auto.quantity, 5)
|
||||
self.assertTrue(move_auto.picked)
|
||||
self.assertEqual(move_manual.manual_consumption, False)
|
||||
self.assertEqual(move_manual.quantity, 5)
|
||||
self.assertTrue(move_manual.picked)
|
||||
|
||||
move_manual.quantity = 6
|
||||
move_manual._onchange_quantity()
|
||||
|
||||
# Now we change quantity to 7. Automatic move will change quantity, but manual move will still be 5 because it has been already picked.
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 7
|
||||
mo = mo_form.save()
|
||||
|
||||
self.assertEqual(move_auto.quantity, 7)
|
||||
self.assertEqual(move_manual.quantity, 6)
|
||||
|
||||
# Bypass consumption issues wizard and create backorders
|
||||
action = mo.button_mark_done()
|
||||
warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context']))
|
||||
consumption = warning.save()
|
||||
action = consumption.action_set_qty()
|
||||
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder_form.save().action_backorder()
|
||||
backorder = mo.production_group_id.production_ids - mo
|
||||
|
||||
# Check that backorders move have the same manual consumption values as BoM
|
||||
move_auto, move_manual = get_moves(backorder)
|
||||
self.assertEqual(move_auto.manual_consumption, False)
|
||||
self.assertEqual(move_manual.manual_consumption, False)
|
||||
|
||||
def test_update_manual_consumption_00(self):
|
||||
"""
|
||||
Check that the manual consumption is set to true when the quantity is manualy set.
|
||||
"""
|
||||
bom = self.bom_1
|
||||
components = bom.bom_line_ids.product_id
|
||||
self.env['stock.quant']._update_available_quantity(components[0], self.stock_location, 10)
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 4
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.move_raw_ids.mapped('manual_consumption'), [False, False])
|
||||
self.assertEqual(components[0].stock_quant_ids.reserved_quantity, 2.0)
|
||||
with Form(mo) as fmo:
|
||||
with fmo.move_raw_ids.edit(0) as line_0:
|
||||
line_0.quantity = 3.0
|
||||
line_0.picked = True
|
||||
self.assertEqual(mo.move_raw_ids.mapped('manual_consumption'), [True, False])
|
||||
self.assertEqual(components[0].stock_quant_ids.reserved_quantity, 3.0)
|
||||
mo.button_mark_done()
|
||||
self.assertRecordValues(mo.move_raw_ids, [{'quantity': 3.0, 'picked': True}, {'quantity': 4.0, 'picked': True}])
|
||||
|
||||
def test_update_manual_consumption_01(self):
|
||||
"""
|
||||
Check that the quantity of a raw line that is manually consumed is not updated
|
||||
when the qty producing is changed and that others are.
|
||||
"""
|
||||
bom = self.bom_1
|
||||
components = bom.bom_line_ids.product_id
|
||||
self.env['stock.quant']._update_available_quantity(components[0], self.stock_location, 10)
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 4
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.move_raw_ids.mapped('manual_consumption'), [False, False])
|
||||
self.assertEqual(components[0].stock_quant_ids.reserved_quantity, 2.0)
|
||||
with Form(mo) as fmo:
|
||||
with fmo.move_raw_ids.edit(0) as line_0:
|
||||
line_0.quantity = 3.0
|
||||
line_0.picked = True
|
||||
fmo.qty_producing = 2.0
|
||||
self.assertEqual(mo.move_raw_ids.mapped('manual_consumption'), [True, False])
|
||||
self.assertEqual(components[0].stock_quant_ids.reserved_quantity, 3.0)
|
||||
self.assertRecordValues(mo.move_raw_ids, [{'quantity': 3.0, 'picked': True}, {'quantity': 2.0, 'picked': True}])
|
||||
|
||||
def test_reservation_state_with_manual_consumption(self):
|
||||
"""
|
||||
Check that the reservation state of an MO is not influenced by moves without demand.
|
||||
"""
|
||||
self.warehouse_1.manufacture_steps = "pbm"
|
||||
bom = self.bom_1
|
||||
components = bom.bom_line_ids.mapped('product_id')
|
||||
components.is_storable = True
|
||||
# make the second component optional
|
||||
bom.bom_line_ids[-1].product_qty = 0.0
|
||||
self.env['stock.quant']._update_available_quantity(components[0], self.warehouse_1.lot_stock_id, 10.0)
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.picking_type_id = self.picking_type_manu
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 4
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertRecordValues(mo.picking_ids.move_ids, [
|
||||
{ "product_id": components[0].id, "product_uom_qty": 2.0}
|
||||
])
|
||||
self.assertEqual(mo.reservation_state, "waiting")
|
||||
mo.picking_ids.button_validate()
|
||||
self.assertEqual(mo.reservation_state, "assigned")
|
||||
mo.move_raw_ids.filtered(lambda m: m.product_id == components[0]).picked = True
|
||||
self.assertEqual(mo.reservation_state, "assigned")
|
||||
|
||||
def test_no_consumption_when_quant_changed(self):
|
||||
"""
|
||||
Test to ensure that from 'Details' wizard, changing only the lot or location
|
||||
of a component move line/quant (without changing the quantity) does not mark it as consumed.
|
||||
|
||||
The wizard (opened via 'action_show_details' on move) should only set
|
||||
'manual_consumption' and 'picked' to True when the done quantity(quantity)
|
||||
differs from the demanded quantity(product_uom_qty).
|
||||
"""
|
||||
bom = self.bom_4
|
||||
component = bom.bom_line_ids.product_id
|
||||
component.write({
|
||||
"is_storable": True,
|
||||
"tracking": "lot",
|
||||
})
|
||||
|
||||
# Create two lots with quants.
|
||||
lots = self.env["stock.lot"].create([
|
||||
{"name": f"lot_{i}", "product_id": component.id} for i in range(2)
|
||||
])
|
||||
for lot in lots:
|
||||
self.env["stock.quant"]._update_available_quantity(
|
||||
component, self.stock_location, 5, lot_id=lot
|
||||
)
|
||||
|
||||
# Create and confirm a Manufacturing Order.
|
||||
mo_form = Form(self.env["mrp.production"])
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
# Initially: not consumed.
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{"manual_consumption": False, "picked": False, "lot_ids": lots[0].ids},
|
||||
])
|
||||
|
||||
# Change only the lot in the 'Details' wizard, keep quantity unchanged.
|
||||
with Form.from_action(self.env, mo.move_raw_ids[0].action_show_details()) as wiz_form:
|
||||
with wiz_form.move_line_ids.edit(0) as move_line:
|
||||
move_line.lot_id = lots[1]
|
||||
wiz_form.save()
|
||||
|
||||
# Still it should not consumed.
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{"manual_consumption": False, "picked": False, "lot_ids": lots[1].ids},
|
||||
])
|
||||
|
||||
# Change the quantity in the 'Details' wizard.
|
||||
with Form.from_action(self.env, mo.move_raw_ids[0].action_show_details()) as wiz_form:
|
||||
with wiz_form.move_line_ids.edit(0) as move_line:
|
||||
move_line.quantity = 2
|
||||
wiz_form.save()
|
||||
|
||||
# Now it should be marked as consumed, since the done quantity differs from the demand.
|
||||
self.assertRecordValues(mo.move_raw_ids, [
|
||||
{"manual_consumption": True, "picked": True, "lot_ids": lots[1].ids},
|
||||
])
|
||||
|
||||
def test_manual_consumption_is_false_if_quantity_was_unchanged(self):
|
||||
"""
|
||||
Check that a move's `manual_consumption` field is only set if the
|
||||
quantity of the move line was modified.
|
||||
"""
|
||||
product_lot = self.env['product.product'].create({
|
||||
'name': 'Product Lot',
|
||||
'is_storable': True,
|
||||
'tracking': 'lot',
|
||||
})
|
||||
lot_1, lot_2 = self.env['stock.lot'].create([{
|
||||
'name': 'lot_1', 'product_id': product_lot.id,
|
||||
}, {
|
||||
'name': 'lot_2', 'product_id': product_lot.id,
|
||||
}])
|
||||
self.env['stock.quant']._update_available_quantity(product_lot, self.stock_location, 2, lot_id=lot_1)
|
||||
self.env['stock.quant']._update_available_quantity(product_lot, self.stock_location, 2, lot_id=lot_2)
|
||||
|
||||
# Create an MO with one component from lot_1, quantity 2
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': self.product.id,
|
||||
'product_qty': 2,
|
||||
'move_raw_ids': [Command.create({
|
||||
'product_id': product_lot.id,
|
||||
'quantity': 2,
|
||||
'move_line_ids': [Command.create({
|
||||
'product_id': product_lot.id,
|
||||
'lot_id': lot_1.id,
|
||||
})],
|
||||
})],
|
||||
})
|
||||
|
||||
# Using the details form, change only the lot of the move line
|
||||
action = mo.move_raw_ids.action_show_details()
|
||||
details_form = Form(mo.move_raw_ids.with_context(action['context']), view=action['view_id'])
|
||||
with details_form.move_line_ids.edit(0) as move_line:
|
||||
move_line.lot_id = lot_2
|
||||
move = details_form.save()
|
||||
# Since quantity was unchanged, `manual_consumption` should not be set
|
||||
self.assertFalse(move.manual_consumption)
|
||||
|
||||
# Use the form again, this time changing the lot and the quantity
|
||||
with details_form.move_line_ids.edit(0) as move_line:
|
||||
move_line.lot_id = lot_1
|
||||
move_line.quantity = 1
|
||||
move = details_form.save()
|
||||
# Quantity was modified, so `manual_consumption` should be set
|
||||
self.assertTrue(move.manual_consumption)
|
||||
|
|
|
|||
41
odoo-bringout-oca-ocb-mrp/mrp/tests/test_mrp_reports.py
Normal file
41
odoo-bringout-oca-ocb-mrp/mrp/tests/test_mrp_reports.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from odoo.tests import HttpCase, tagged
|
||||
from odoo.fields import Command
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestReportBom(HttpCase):
|
||||
|
||||
def test_mrp_report_bom_variant_selection(self):
|
||||
self.env.ref('base.user_admin').write({'group_ids': [
|
||||
Command.link(self.env.ref('product.group_product_variant').id),
|
||||
]})
|
||||
|
||||
attribute = self.env['product.attribute'].create({'name': 'Size'})
|
||||
value_S, value_L = self.env['product.attribute.value'].create([
|
||||
{'name': 'S', 'attribute_id': attribute.id},
|
||||
{'name': 'L', 'attribute_id': attribute.id}
|
||||
])
|
||||
|
||||
product_tmpl = self.env['product.template'].create({
|
||||
'name': 'Product Test Sync',
|
||||
'type': 'consu',
|
||||
'attribute_line_ids': [Command.create({
|
||||
'attribute_id': attribute.id,
|
||||
'value_ids': [Command.set([value_S.id, value_L.id])]
|
||||
})]
|
||||
})
|
||||
|
||||
[variant_s, variant_l] = product_tmpl.product_variant_ids
|
||||
|
||||
variant_s.default_code = 'zebra'
|
||||
variant_l.default_code = 'alpaca'
|
||||
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': product_tmpl.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
})
|
||||
|
||||
action_id = self.env.ref('mrp.action_report_mrp_bom')
|
||||
url = "/web#action=%s&active_id=%s" % (str(action_id.id), str(bom.id))
|
||||
self.start_tour(url, "mrp_bom_report_tour", login="admin")
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import common, Form
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
|
@ -24,14 +25,14 @@ class TestMrpMulticompany(common.TransactionCase):
|
|||
cls.user_a = cls.env['res.users'].create({
|
||||
'name': 'user company a with access to company b',
|
||||
'login': 'user a',
|
||||
'groups_id': [(6, 0, [group_user.id, group_mrp_manager.id])],
|
||||
'group_ids': [(6, 0, [group_user.id, group_mrp_manager.id])],
|
||||
'company_id': cls.company_a.id,
|
||||
'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
|
||||
})
|
||||
cls.user_b = cls.env['res.users'].create({
|
||||
'name': 'user company a with access to company b',
|
||||
'login': 'user b',
|
||||
'groups_id': [(6, 0, [group_user.id, group_mrp_manager.id])],
|
||||
'group_ids': [(6, 0, [group_user.id, group_mrp_manager.id])],
|
||||
'company_id': cls.company_b.id,
|
||||
'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
|
||||
})
|
||||
|
|
@ -135,12 +136,12 @@ class TestMrpMulticompany(common.TransactionCase):
|
|||
})
|
||||
mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
|
||||
mo_form.product_id = product
|
||||
# The mo must be confirmed, no longer in draft, in order for `lot_producing_id` to be visible in the view
|
||||
# <div class="o_row" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}">
|
||||
# The mo must be confirmed, no longer in draft, in order for `lot_producing_ids` to be visible in the view
|
||||
# <div class="o_row" invisible="state == 'draft' or product_tracking in ('none', False)">
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.lot_producing_id = lot_b
|
||||
mo_form.lot_producing_ids.set(lot_b)
|
||||
mo = mo_form.save()
|
||||
with self.assertRaises(UserError):
|
||||
mo.with_user(self.user_b).action_confirm()
|
||||
|
|
@ -176,8 +177,9 @@ class TestMrpMulticompany(common.TransactionCase):
|
|||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = lot_b
|
||||
ml.qty_done = 1
|
||||
ml.quantity = 1
|
||||
details_operation_form.save()
|
||||
mo.move_raw_ids.picked = True
|
||||
with self.assertRaises(UserError):
|
||||
mo.button_mark_done()
|
||||
|
||||
|
|
@ -205,8 +207,6 @@ class TestMrpMulticompany(common.TransactionCase):
|
|||
self.assertFalse(product1.with_company(self.company_b).is_kits)
|
||||
self.assertFalse(template1.with_company(self.company_b).is_kits)
|
||||
|
||||
self.assertTrue(product2.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
|
||||
self.assertTrue(template2.with_context(allowed_company_ids=[self.company_b.id, self.company_a.id]).is_kits)
|
||||
self.assertTrue(product2.with_company(self.company_a).is_kits)
|
||||
self.assertTrue(template2.with_company(self.company_a).is_kits)
|
||||
self.assertTrue(product2.with_company(self.company_b).is_kits)
|
||||
|
|
@ -255,7 +255,7 @@ class TestMrpMulticompany(common.TransactionCase):
|
|||
""" Check that we are able to create a new warehouse when the generic manufacture route
|
||||
is in a different company. """
|
||||
group_stock_manager = self.env.ref('stock.group_stock_manager')
|
||||
self.user_a.write({'groups_id': [(4, group_stock_manager.id)]})
|
||||
self.user_a.write({'group_ids': [(4, group_stock_manager.id)]})
|
||||
|
||||
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture')
|
||||
for rule in manufacture_route.rule_ids.sudo():
|
||||
|
|
@ -282,3 +282,36 @@ class TestMrpMulticompany(common.TransactionCase):
|
|||
'code': 'WH2',
|
||||
})
|
||||
self.assertEqual(new_warehouse.manufacture_pull_id.route_id.company_id, self.company_b)
|
||||
|
||||
def test_multi_company_kit_reservation(self):
|
||||
"""
|
||||
Create and assign a delivery in company_b for a product that is a kit in company_a.
|
||||
Check that the move is treated just as a non-kit product.
|
||||
"""
|
||||
""" Check that is_kits is company dependant """
|
||||
semi_kit_product = self.env['product.product'].create({
|
||||
'name': 'Kit Kat',
|
||||
'is_storable': True,
|
||||
})
|
||||
self.env['mrp.bom'].create([{
|
||||
'product_id': semi_kit_product.id,
|
||||
'product_tmpl_id': semi_kit_product.product_tmpl_id.id,
|
||||
'company_id': self.company_a.id,
|
||||
'type': 'phantom',
|
||||
}])
|
||||
warehouse_b = self.env['stock.warehouse'].search([('company_id', '=', self.company_b.id)], limit=1)
|
||||
delivery = self.env['stock.picking'].with_company(self.company_b.id).create({
|
||||
'picking_type_id': warehouse_b.out_type_id.id,
|
||||
'location_id': warehouse_b.lot_stock_id.id,
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
'move_ids': [Command.create({
|
||||
'product_id': semi_kit_product.id,
|
||||
'product_uom_qty': 1,
|
||||
'location_id': warehouse_b.lot_stock_id.id,
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
})]
|
||||
})
|
||||
# confirm and assign the delivery with company_a and check that it was treated as a non-kit product
|
||||
delivery.with_company(self.company_a).action_confirm()
|
||||
delivery.with_company(self.company_a).action_assign()
|
||||
self.assertRecordValues(delivery.move_ids, [{'state': 'confirmed', 'quantity': 0.0}])
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta, time
|
||||
from freezegun import freeze_time
|
||||
from pytz import timezone, utc
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestOee(TestMrpCommon):
|
||||
|
|
@ -18,9 +20,26 @@ class TestOee(TestMrpCommon):
|
|||
'description': loss_reason.name
|
||||
})
|
||||
|
||||
def test_wrokcenter_oee(self):
|
||||
@freeze_time('2025-05-30')
|
||||
def test_unset_end_date(self):
|
||||
with Form(self.env['mrp.workcenter.productivity']) as workcenter_productivity:
|
||||
# Set the end date to tomorrow
|
||||
workcenter_productivity.date_end = datetime(2025, 5, 31, 12, 0, 0)
|
||||
# Unset the end date
|
||||
workcenter_productivity.date_end = False
|
||||
self.assertFalse(workcenter_productivity.date_end)
|
||||
self.assertEqual(workcenter_productivity.duration, 0.0, "The duration should be 0.0 when the end date is unset.")
|
||||
|
||||
workcenter_productivity.workcenter_id = self.workcenter_1
|
||||
workcenter_productivity.date_end = datetime(2025, 5, 31, 12, 0, 0)
|
||||
workcenter_productivity.date_start = datetime(2025, 5, 30, 12, 0, 0)
|
||||
workcenter_productivity.save()
|
||||
self.assertEqual(workcenter_productivity.duration, 1440.0)
|
||||
|
||||
def test_workcenter_oee(self):
|
||||
""" Test case workcenter oee. """
|
||||
day = datetime.date(datetime.today())
|
||||
self.workcenter_1.resource_calendar_id.leave_ids.unlink()
|
||||
# Make the test work the weekend. It will fails due to workcenter working hours.
|
||||
if day.weekday() in (5, 6):
|
||||
day -= timedelta(days=2)
|
||||
|
|
@ -57,15 +76,15 @@ class TestOee(TestMrpCommon):
|
|||
end_time = time_to_string_utc_datetime(time(10, 53, 22))
|
||||
self.create_productivity_line(self.env.ref('mrp.block_reason4'), start_time, end_time)
|
||||
|
||||
# Block time : ( Process Defact (1.33 min) + Reduced Speed (3.0 min) + Material Availability (1.52 min)) = 5.85 min
|
||||
blocked_time_in_hour = round(((1.33 + 3.0 + 1.52) / 60.0), 2)
|
||||
# Blocked time : ( Process Defect (1.33 min) + Reduced Speed (3.0 min) + Material Availability (1.52 min)) = 5.85 min
|
||||
blocked_time = 1.33 + 3.0 + 1.52
|
||||
# Productive time : Productive time duration (13 min)
|
||||
productive_time_in_hour = round((13.0 / 60.0), 2)
|
||||
productive_time = 13.0
|
||||
|
||||
# Check blocked time and productive time
|
||||
self.assertEqual(self.workcenter_1.blocked_time, blocked_time_in_hour, "Wrong block time on workcenter.")
|
||||
self.assertEqual(self.workcenter_1.productive_time, productive_time_in_hour, "Wrong productive time on workcenter.")
|
||||
# Blocked & Productive time are rounded to 2 digits when computed
|
||||
self.assertEqual(self.workcenter_1.blocked_time, round(blocked_time / 60, 2), "Wrong blocked time on workcenter.")
|
||||
self.assertEqual(self.workcenter_1.productive_time, round(productive_time / 60, 2), "Wrong productive time on workcenter.")
|
||||
|
||||
# Check overall equipment effectiveness
|
||||
computed_oee = round(((productive_time_in_hour * 100.0)/(productive_time_in_hour + blocked_time_in_hour)), 2)
|
||||
# OEE is not calculated with intermediary rounding
|
||||
computed_oee = round((((productive_time / 60) * 100.0) / ((productive_time / 60) + (blocked_time / 60))), 2)
|
||||
self.assertEqual(self.workcenter_1.oee, computed_oee, "Wrong oee on workcenter.")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -31,12 +31,12 @@ class TestMrpSerialMassProducePerformance(common.TransactionCase):
|
|||
for i in range(raw_materials_count):
|
||||
raw_materials.append(self.env['product.product'].create({
|
||||
'name': '@raw_material#' + str(i + 1),
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': trackings[i % len(trackings)]
|
||||
}))
|
||||
finished = self.env['product.product'].create({
|
||||
'name': '@finished',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
|
|
@ -70,7 +70,6 @@ class TestMrpSerialMassProducePerformance(common.TransactionCase):
|
|||
while qty > 0:
|
||||
lot = self.env['stock.lot'].create({
|
||||
'product_id': raw_materials[i].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': raw_materials[i].id,
|
||||
|
|
@ -83,7 +82,6 @@ class TestMrpSerialMassProducePerformance(common.TransactionCase):
|
|||
for _ in range(total_quantity):
|
||||
lot = self.env['stock.lot'].create({
|
||||
'product_id': raw_materials[i].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': raw_materials[i].id,
|
||||
|
|
@ -94,7 +92,7 @@ class TestMrpSerialMassProducePerformance(common.TransactionCase):
|
|||
|
||||
mo.action_assign()
|
||||
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
action = mo.action_mass_produce()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = quantity
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
55
odoo-bringout-oca-ocb-mrp/mrp/tests/test_quant.py
Normal file
55
odoo-bringout-oca-ocb-mrp/mrp/tests/test_quant.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
from odoo import Command
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
|
||||
|
||||
class TestMrpQuant(TestMrpCommon):
|
||||
|
||||
def test_kit_product_reservation_flow(self):
|
||||
"""Test that kit products are not cleaned"""
|
||||
|
||||
product_kit = self.env['product.product'].create({
|
||||
'name': 'This product will be a kit',
|
||||
'type': 'consu',
|
||||
'is_storable': True
|
||||
})
|
||||
|
||||
delivery = self.env['stock.picking'].create({
|
||||
'location_id': self.shelf_1.id,
|
||||
'location_dest_id': self.warehouse_1.lot_stock_id.id,
|
||||
'picking_type_id': self.picking_type_out.id,
|
||||
'move_line_ids': [Command.create({
|
||||
'product_id': product_kit.id,
|
||||
'quantity_product_uom': 1,
|
||||
'location_dest_id': self.warehouse_1.lot_stock_id.id,
|
||||
})]
|
||||
})
|
||||
|
||||
delivery.action_confirm()
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': product_kit.product_tmpl_id.id,
|
||||
'product_id': product_kit.id,
|
||||
'product_qty': 1,
|
||||
'type': 'phantom', # type kit
|
||||
})
|
||||
|
||||
# Force recomputation of is_kits, since it's currently not recomputed automatically when BOM is created
|
||||
product_kit._compute_is_kits()
|
||||
delivery.move_ids.quantity = 1
|
||||
product_normal = self.env['product.product'].create({
|
||||
'name': 'Normal Product Test',
|
||||
'type': 'consu',
|
||||
'is_storable': True
|
||||
})
|
||||
|
||||
# Should work without error
|
||||
product_normal.action_open_quants()
|
||||
|
||||
# Ensure it's raise an error if we try to update quantity of kit product directly
|
||||
with self.assertRaises(UserError, msg="You should update the components quantity instead of directly updating the quantity of the kit product."):
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
product_kit,
|
||||
self.warehouse_1.lot_stock_id,
|
||||
10,
|
||||
)
|
||||
398
odoo-bringout-oca-ocb-mrp/mrp/tests/test_replenish.py
Normal file
398
odoo-bringout-oca-ocb-mrp/mrp/tests/test_replenish.py
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
from json import loads
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo import fields, Command
|
||||
|
||||
|
||||
|
||||
class TestMrpReplenish(TestMrpCommon):
|
||||
|
||||
def _create_wizard(self, product, warehouse):
|
||||
return self.env['product.replenish'].with_context(default_product_tmpl_id=product.product_tmpl_id.id).create({
|
||||
'product_id': product.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'quantity': 1,
|
||||
'warehouse_id': warehouse.id,
|
||||
})
|
||||
|
||||
def test_mrp_delay(self):
|
||||
"""Open the replenish view and check if delay is taken into account
|
||||
in the base date computation
|
||||
"""
|
||||
route = self.warehouse_1.manufacture_pull_id.route_id
|
||||
product = self.product_4
|
||||
product.route_ids = route
|
||||
|
||||
with freeze_time("2023-01-01"):
|
||||
wizard = self._create_wizard(product, self.warehouse_1)
|
||||
self.assertEqual(fields.Datetime.from_string('2023-01-01 00:00:00'), wizard.date_planned)
|
||||
route.rule_ids[0].delay = 2
|
||||
wizard3 = self._create_wizard(product, self.warehouse_1)
|
||||
self.assertEqual(fields.Datetime.from_string('2023-01-03 00:00:00'), wizard3.date_planned)
|
||||
|
||||
def test_mrp_orderpoint_leadtime(self):
|
||||
self.env.company.horizon_days = 0
|
||||
route_manufacture = self.warehouse_1.manufacture_pull_id.route_id
|
||||
route_manufacture.supplied_wh_id = self.warehouse_1
|
||||
route_manufacture.supplier_wh_id = self.warehouse_1
|
||||
route_manufacture.rule_ids.delay = 2
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'Cake',
|
||||
'is_storable': True,
|
||||
'route_ids': [(6, 0, [route_manufacture.id])]
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': product_1.product_tmpl_id.id,
|
||||
'produce_delay': 4,
|
||||
'product_qty': 1,
|
||||
})
|
||||
|
||||
# setup orderpoint (reordering rule)
|
||||
rr = self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Cake RR',
|
||||
'location_id': self.stock_location.id,
|
||||
'product_id': product_1.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 5,
|
||||
})
|
||||
|
||||
info = self.env['stock.replenishment.info'].create({'orderpoint_id': rr.id})
|
||||
|
||||
# for manufacturing delay should be taken from the bom
|
||||
self.assertEqual("4.0 days", info.wh_replenishment_option_ids.lead_time)
|
||||
|
||||
def test_rr_picking_type_id(self):
|
||||
"""Check manufacturing order take bom according to picking type of the rule triggered by an
|
||||
orderpoint."""
|
||||
|
||||
self.product_4.route_ids = self.warehouse_1.manufacture_pull_id.route_id
|
||||
picking_type_2 = self.picking_type_manu.copy({'sequence': 100})
|
||||
self.product_4.bom_ids.picking_type_id = picking_type_2
|
||||
rr = self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Cake RR',
|
||||
'location_id': self.warehouse_1.lot_stock_id.id,
|
||||
'product_id': self.product_4.id,
|
||||
'product_min_qty': 5,
|
||||
'product_max_qty': 10,
|
||||
})
|
||||
|
||||
rr.action_replenish()
|
||||
mo = self.env['mrp.production'].search([
|
||||
('product_id', '=', self.product_4.id),
|
||||
('picking_type_id', '=', picking_type_2.id)
|
||||
])
|
||||
self.assertTrue(mo)
|
||||
|
||||
def test_mrp_delay_bom(self):
|
||||
route = self.warehouse_1.manufacture_pull_id.route_id
|
||||
product = self.product_4
|
||||
bom = product.bom_ids
|
||||
product.route_ids = route
|
||||
with freeze_time("2023-01-01"):
|
||||
wizard = self._create_wizard(product, self.warehouse_1)
|
||||
self.assertEqual(fields.Datetime.from_string('2023-01-01 00:00:00'), wizard.date_planned)
|
||||
bom.produce_delay = 2
|
||||
wizard2 = self._create_wizard(product, self.warehouse_1)
|
||||
self.assertEqual(fields.Datetime.from_string('2023-01-03 00:00:00'), wizard2.date_planned)
|
||||
bom.days_to_prepare_mo = 4
|
||||
wizard3 = self._create_wizard(product, self.warehouse_1)
|
||||
self.assertEqual(fields.Datetime.from_string('2023-01-07 00:00:00'), wizard3.date_planned)
|
||||
|
||||
def test_replenish_from_scrap(self):
|
||||
""" Test that when ticking replenish on the scrap wizard of a MO, the new move
|
||||
is linked to the MO and validating it will automatically reserve the quantity
|
||||
on the MO. """
|
||||
self.warehouse_1.manufacture_steps = 'pbm'
|
||||
basic_mo, dummy1, dummy2, product_to_scrap, other_product = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
|
||||
for product in (product_to_scrap, other_product):
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': product.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'quantity': 2
|
||||
})
|
||||
self.assertEqual(basic_mo.move_raw_ids.location_id, self.warehouse_1.pbm_loc_id)
|
||||
basic_mo.action_confirm()
|
||||
self.assertEqual(len(basic_mo.picking_ids), 1)
|
||||
basic_mo.picking_ids.action_assign()
|
||||
basic_mo.picking_ids.button_validate()
|
||||
self.assertEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
|
||||
|
||||
scrap_form = Form.from_action(self.env, basic_mo.button_scrap())
|
||||
scrap_form.product_id = product_to_scrap
|
||||
scrap_form.should_replenish = True
|
||||
self.assertEqual(scrap_form.location_id, self.warehouse_1.pbm_loc_id)
|
||||
scrap_form.save().action_validate()
|
||||
self.assertNotEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
|
||||
self.assertEqual(len(basic_mo.picking_ids), 2)
|
||||
replenish_picking = basic_mo.picking_ids.filtered(lambda x: x.state == 'assigned')
|
||||
replenish_picking.button_validate()
|
||||
self.assertEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
|
||||
|
||||
def test_scrap_replenishment_reassigns_required_qty_to_component(self):
|
||||
""" Test that when validating the scrap replenishment transfer, the required quantity
|
||||
is re-assigned to the component for the final manufacturing product. """
|
||||
self.warehouse_1.manufacture_steps = 'pbm'
|
||||
basic_mo, _, _, product_to_scrap, other_product = self.generate_mo(qty_final=1, qty_base_1=10, qty_base_2=10)
|
||||
for product in (product_to_scrap, other_product):
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': product.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'quantity': 20
|
||||
})
|
||||
self.assertEqual(basic_mo.move_raw_ids.location_id, self.warehouse_1.pbm_loc_id)
|
||||
basic_mo.action_confirm()
|
||||
self.assertEqual(len(basic_mo.picking_ids), 1)
|
||||
basic_mo.picking_ids.action_assign()
|
||||
basic_mo.picking_ids.button_validate()
|
||||
self.assertEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
|
||||
|
||||
# Scrap the product and trigger replenishment
|
||||
scrap_form = Form.from_action(self.env, basic_mo.button_scrap())
|
||||
scrap_form.product_id = product_to_scrap
|
||||
scrap_form.scrap_qty = 5
|
||||
scrap_form.should_replenish = True
|
||||
self.assertEqual(scrap_form.location_id, self.warehouse_1.pbm_loc_id)
|
||||
scrap_form.save().action_validate()
|
||||
|
||||
# Assert that the component quantity is reduced
|
||||
self.assertNotEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
|
||||
move_to_scrap = basic_mo.move_raw_ids.filtered(lambda m: m.product_id == product_to_scrap)
|
||||
move_other = basic_mo.move_raw_ids.filtered(lambda m: m.product_id == other_product)
|
||||
self.assertEqual(move_to_scrap.quantity, 5, "Scrapped component should have qty 5")
|
||||
self.assertEqual(move_other.quantity, 10, "Other component should remain qty 10")
|
||||
self.assertEqual(len(basic_mo.picking_ids), 2)
|
||||
|
||||
replenish_picking = basic_mo.picking_ids.filtered(lambda x: x.state == 'assigned')
|
||||
replenish_picking.button_validate()
|
||||
|
||||
# Assert that the component quantity is re-assigned
|
||||
self.assertEqual(basic_mo.move_raw_ids.mapped('state'), ['assigned', 'assigned'])
|
||||
self.assertEqual(move_to_scrap.quantity, 10, "Scrapped component should return to qty 10")
|
||||
self.assertEqual(move_other.quantity, 10, "Other component should still be qty 10")
|
||||
|
||||
def test_global_horizon_days_affect_lead_time_manufacture_rule(self):
|
||||
""" Ensure global horizon days will only be captured one time in an orderpoint's
|
||||
lead_days/json_lead_days.
|
||||
"""
|
||||
self.warehouse_1.manufacture_steps = 'pbm'
|
||||
finished_product = self.product_4
|
||||
finished_product.route_ids = [Command.set(self.warehouse_1.manufacture_pull_id.route_id.ids)]
|
||||
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||||
'product_id': finished_product.id,
|
||||
'location_id': self.stock_location.id,
|
||||
})
|
||||
out_picking = self.env['stock.picking'].create({
|
||||
'picking_type_id': self.picking_type_out.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'move_ids': [Command.create({
|
||||
'product_id': finished_product.id,
|
||||
'product_uom_qty': 2,
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
})],
|
||||
})
|
||||
out_picking.with_context(global_horizon_days=365).action_assign()
|
||||
r = orderpoint.action_stock_replenishment_info()
|
||||
repl_info = self.env[r['res_model']].browse(r['res_id'])
|
||||
lead_horizon_date = datetime.strptime(
|
||||
loads(repl_info.json_lead_days)['lead_horizon_date'], '%m/%d/%Y').date()
|
||||
self.assertEqual(lead_horizon_date, fields.Date.today() + timedelta(days=365))
|
||||
|
||||
def test_orderpoint_onchange_reordering_rule(self):
|
||||
""" Ensure onchange logic works properly when editing a reordering rule
|
||||
linked to a confirmed MO, which is started but not finished by the
|
||||
end of the stock forecast.
|
||||
"""
|
||||
route_manufacture = self.warehouse_1.manufacture_pull_id.route_id
|
||||
|
||||
self.product_4.route_ids = [Command.set([route_manufacture.id])]
|
||||
self.product_4.bom_ids.produce_delay = 2
|
||||
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||||
'product_id': self.product_4.id,
|
||||
'product_min_qty': 2,
|
||||
'product_max_qty': 2,
|
||||
})
|
||||
|
||||
orderpoint.action_replenish()
|
||||
|
||||
prod = self.env['mrp.production'].search([('origin', '=', orderpoint.name)])
|
||||
# Error is triggered for date_start <= lead_horizon_date < date_finished
|
||||
prod.date_start = fields.Date.today() + timedelta(days=1)
|
||||
|
||||
with Form(orderpoint, view='stock.view_warehouse_orderpoint_tree_editable') as form:
|
||||
form.product_min_qty = 3
|
||||
self.assertEqual(orderpoint.qty_to_order, 1)
|
||||
|
||||
orderpoint.trigger = 'manual'
|
||||
with Form(orderpoint, view='stock.view_warehouse_orderpoint_tree_editable') as form:
|
||||
form.product_min_qty = 10
|
||||
self.assertEqual(form.qty_to_order, 0)
|
||||
self.assertEqual(form.qty_to_order, 8)
|
||||
self.assertEqual(orderpoint.qty_to_order, 8)
|
||||
|
||||
def test_replenish_multi_level_bom_with_pbm_sam(self):
|
||||
"""
|
||||
Ensure that in a 3-step manufacturing flow ('pbm_sam') with MTO + reordering rule,
|
||||
a multi-level BOM triggers separate MOs for each level without constraint errors.
|
||||
1.) Set warehouse manufacture to (manufacture_steps == 'pbm_sam')
|
||||
2.) Product_1 (enable manufacture and mto routes)
|
||||
3.) Product_4 (enable manufacture)
|
||||
4.) Add Product_1 as bom for product_4
|
||||
5.) Add a reordering rule (manufacture) for product_4
|
||||
6.) trigger replenishment for product_4
|
||||
"""
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
self.warehouse.write({'manufacture_steps': 'pbm_sam'})
|
||||
self.warehouse.mto_pull_id.route_id.active = True
|
||||
|
||||
route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
|
||||
route_mto = self.warehouse.mto_pull_id.route_id.id
|
||||
|
||||
self.product_1.write({
|
||||
'route_ids': [(6, 0, [route_mto, route_manufacture])]
|
||||
}) # Component
|
||||
self.product_4.write({
|
||||
'route_ids': [(6, 0, [route_manufacture])],
|
||||
'bom_ids': [(6, 0, [self.bom_1.id])]
|
||||
}) # Finished Product
|
||||
|
||||
# Create reordering rule
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'product_id': self.product_4.id,
|
||||
'route_id': route_manufacture,
|
||||
'product_min_qty': 1,
|
||||
'product_max_qty': 1,
|
||||
})
|
||||
self.product_4.orderpoint_ids.action_replenish()
|
||||
|
||||
# Check both MOs were created
|
||||
mo_final = self.env['mrp.production'].search([('product_id', '=', self.product_4.id)])
|
||||
mo_component = self.env['mrp.production'].search([('product_id', '=', self.product_1.id)])
|
||||
|
||||
self.assertEqual(len(mo_final), 1, "Expected one MO for the final product.")
|
||||
self.assertEqual(len(mo_component), 1, "Expected one MO for the manufactured BOM component.")
|
||||
|
||||
def test_orderpoint_warning_mrp(self):
|
||||
""" Checks that the warning correctly computes depending on if there's a bom. """
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||||
'product_id': self.product_4.id,
|
||||
'product_min_qty': 10,
|
||||
'product_max_qty': 50,
|
||||
})
|
||||
self.product_4.bom_ids.active = False
|
||||
self.assertTrue(orderpoint.show_supply_warning)
|
||||
|
||||
# Archive the boms linked to the product
|
||||
self.product_4.with_context(active_test=False).bom_ids.active = True
|
||||
orderpoint.invalidate_recordset(fnames=['rule_ids', 'show_supply_warning'])
|
||||
self.assertFalse(orderpoint.show_supply_warning)
|
||||
|
||||
# Add a manufacture route to the product
|
||||
self.product_4.route_ids |= self.route_manufacture
|
||||
orderpoint.invalidate_recordset(fnames=['show_supply_warning'])
|
||||
self.assertFalse(orderpoint.show_supply_warning)
|
||||
|
||||
def test_set_bom_on_orderpoint(self):
|
||||
""" Test that action_set_bom_on_orderpoint correctly sets a bom on selected orderpoint. """
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||||
'product_id': self.product_4.id,
|
||||
'product_min_qty': 10,
|
||||
'product_max_qty': 50,
|
||||
})
|
||||
self.product_4.bom_ids.with_context(orderpoint_id=orderpoint.id).action_set_bom_on_orderpoint()
|
||||
self.assertEqual(orderpoint.bom_id.id, self.product_4.bom_ids.id)
|
||||
|
||||
def test_effective_bom(self):
|
||||
route = self.env['stock.route'].create({
|
||||
'name': 'Manufacture',
|
||||
'rule_ids': [
|
||||
Command.create({
|
||||
'name': 'Manufacture',
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'action': 'manufacture',
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||||
'product_id': self.productA.id,
|
||||
'product_min_qty': 2,
|
||||
'product_max_qty': 4,
|
||||
})
|
||||
# The Manufacture route is not set on the product -> no effective BoM
|
||||
self.assertFalse(orderpoint.effective_bom_id)
|
||||
|
||||
self.productA.write({
|
||||
'route_ids': [(4, route.id)],
|
||||
})
|
||||
self.env.invalidate_all()
|
||||
# The route is set, but there is no BoM -> no effective BoM
|
||||
self.assertFalse(orderpoint.effective_bom_id)
|
||||
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.productA.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'code': 'Ref 1234',
|
||||
})
|
||||
|
||||
self.env.invalidate_all()
|
||||
# The route is set and there is a BoM -> effective BoM is available
|
||||
self.assertEqual(orderpoint.effective_bom_id, bom)
|
||||
self.assertEqual(orderpoint.bom_id_placeholder, 'Ref 1234: Product A')
|
||||
# The actual BoM remains empty
|
||||
self.assertFalse(orderpoint.bom_id)
|
||||
|
||||
def test_lead_time_with_no_bom(self):
|
||||
"""Test that lead time is incremented by 365 days (1 year) when there
|
||||
is no BoM defined.
|
||||
"""
|
||||
route_manufacture = self.warehouse_1.manufacture_pull_id.route_id
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'test',
|
||||
'is_storable': True,
|
||||
'route_ids': route_manufacture.ids,
|
||||
})
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'test',
|
||||
'location_id': self.warehouse_1.lot_stock_id.id,
|
||||
'product_id': product.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 5,
|
||||
})
|
||||
self.assertEqual(orderpoint.lead_days, 365)
|
||||
|
||||
def test_orderpoint_with_kit_bom_in_another_company(self):
|
||||
"""Test that an orderpoint can be created for a product
|
||||
having a kit-type BoM defined in another company.
|
||||
"""
|
||||
self.assertEqual(self.bom_2.type, 'phantom')
|
||||
self.assertEqual(self.bom_2.company_id, self.env.company)
|
||||
company_2 = self.env['res.company'].create({'name': 'Company 2'})
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].with_company(company_2).create({
|
||||
'product_id': self.bom_2.product_id.id,
|
||||
})
|
||||
self.assertEqual(orderpoint.company_id, company_2)
|
||||
|
||||
def test_product_replenish_wizard_multiple_manufacture_routes(self):
|
||||
self.route_manufacture.copy()
|
||||
wizard_form = Form(self.env['product.replenish'].with_context(
|
||||
default_product_tmpl_id=self.product_4.product_tmpl_id.id
|
||||
))
|
||||
manufacture_route = self.env['stock.rule'].search([
|
||||
('action', '=', 'manufacture'),
|
||||
('company_id', '=', self.company.id),
|
||||
('location_dest_id.usage', '=', 'internal'),
|
||||
], limit=1).route_id
|
||||
self.assertEqual(wizard_form.route_id, manufacture_route)
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import Form
|
||||
from odoo import Command
|
||||
|
||||
|
||||
class TestMrpSerialMassProduce(TestMrpCommon):
|
||||
|
||||
def test_smp_serial(self):
|
||||
"""Create a MO for a product not tracked by serial number.
|
||||
The smp wizard should not open.
|
||||
"""
|
||||
mo = self.generate_mo()[0]
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
res = mo.action_serial_mass_produce_wizard()
|
||||
self.assertFalse(res)
|
||||
|
||||
def test_smp_produce_all(self):
|
||||
"""Create a MO for a product tracked by serial number.
|
||||
Open the smp wizard, generate all serial numbers to produce all quantities.
|
||||
"""
|
||||
mo = self.generate_mo(tracking_final='serial')[0]
|
||||
count = mo.product_qty
|
||||
# Make some stock and reserve
|
||||
for product in mo.move_raw_ids.product_id:
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': 100,
|
||||
'location_id': mo.location_src_id.id,
|
||||
})._apply_inventory()
|
||||
mo.action_assign()
|
||||
# Open the wizard
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
# Let the wizard generate all serial numbers
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = count
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to apply generated serial numbers
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().apply()
|
||||
# Initial MO should have a backorder-sequenced name and be in to_close state
|
||||
self.assertTrue("-001" in mo.name)
|
||||
self.assertEqual(mo.state, "to_close")
|
||||
# Each generated serial number should have its own mo
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), count)
|
||||
# Check generated serial numbers
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.lot_producing_id.mapped('name'), ["sn#1", "sn#2", "sn#3", "sn#4", "sn#5"])
|
||||
|
||||
def test_smp_produce_all_but_one(self):
|
||||
"""Create a MO for a product tracked by serial number.
|
||||
Open the smp wizard, generate all but one serial numbers and create a back order.
|
||||
"""
|
||||
mo = self.generate_mo(tracking_final='serial')[0]
|
||||
count = mo.product_qty
|
||||
# Make some stock and reserve
|
||||
for product in mo.move_raw_ids.product_id:
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': 100,
|
||||
'location_id': mo.location_src_id.id,
|
||||
})._apply_inventory()
|
||||
mo.action_assign()
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = count - 1
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to create backorder (applying generated serial numbers)
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().create_backorder()
|
||||
# Last MO in sequence is the backorder
|
||||
bo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(bo.backorder_sequence, count)
|
||||
self.assertEqual(bo.state, "confirmed")
|
||||
|
||||
def test_smp_produce_complex(self):
|
||||
"""Create a MO for a product tracked by serial number
|
||||
and with complex components (serial and multiple lots).
|
||||
Open the smp wizard, generate all serial numbers to produce all quantities.
|
||||
Check lot splitting.
|
||||
"""
|
||||
mo, dummy, dummy, product_to_use_1, product_to_use_2 = self.generate_mo(tracking_final='serial', tracking_base_1='lot', tracking_base_2='serial', qty_final=3, qty_base_1=2, qty_base_2=1)
|
||||
count = mo.product_qty
|
||||
# Make some stock and reserve
|
||||
for _ in range(2): # 2 lots of 3 to satisfy the need and check lot splitting
|
||||
lot = self.env['stock.lot'].create({
|
||||
'product_id': product_to_use_1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product_to_use_1.id,
|
||||
'inventory_quantity': 3,
|
||||
'location_id': mo.location_src_id.id,
|
||||
'lot_id': lot.id,
|
||||
})._apply_inventory()
|
||||
for _ in range(3): # 3 serial numbers
|
||||
lot = self.env['stock.lot'].create({
|
||||
'product_id': product_to_use_2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product_to_use_2.id,
|
||||
'inventory_quantity': 1,
|
||||
'location_id': mo.location_src_id.id,
|
||||
'lot_id': lot.id,
|
||||
})._apply_inventory()
|
||||
mo.action_assign()
|
||||
# Open the wizard
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
# Let the wizard generate all serial numbers
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = count
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to apply generated serial numbers
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().apply()
|
||||
# 1st & 3rd MO in sequence should have only 1 move lines (1 lot) for product_to_use_1 (2nd in bom)
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids[0].move_raw_ids[1].move_lines_count, 1)
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids[2].move_raw_ids[1].move_lines_count, 1)
|
||||
# 2nd MO should have 2 move lines (2 different lots) for product_to_use_1
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids[1].move_raw_ids[1].move_lines_count, 2)
|
||||
|
||||
def test_mass_produce_with_tracked_product(self):
|
||||
"""
|
||||
Check that we can mass produce a tracked product.
|
||||
"""
|
||||
tracked_product = self.env['product.product'].create({
|
||||
'name': 'Tracked Product',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Component',
|
||||
'type': 'product',
|
||||
})
|
||||
byproduct = self.env['product.product'].create({
|
||||
'name': 'Byproduct',
|
||||
'type': 'product',
|
||||
})
|
||||
# create a BoM
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': tracked_product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_line_ids': [Command.create({
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
'byproduct_ids': [Command.create({
|
||||
'product_id': byproduct.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
})
|
||||
sn_1 = self.env['stock.lot'].create({
|
||||
'name': 'SN1',
|
||||
'product_id': tracked_product.id,
|
||||
})
|
||||
sn_2 = self.env['stock.lot'].create({
|
||||
'name': 'SN2',
|
||||
'product_id': tracked_product.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(tracked_product, self.stock_location_14, 1, lot_id=sn_1)
|
||||
self.env['stock.quant']._update_available_quantity(tracked_product, self.stock_location_14, 1, lot_id=sn_2)
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location_14, 10)
|
||||
# create an MO to use the tracked product available in stock
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_1
|
||||
mo_form.product_qty = 2
|
||||
mo_form.product_uom_id = component.uom_id
|
||||
# use tracked as component
|
||||
with mo_form.move_raw_ids.new() as move:
|
||||
move.name = tracked_product.name
|
||||
move.product_id = tracked_product
|
||||
move.product_uom_qty = 2
|
||||
move.product_uom = tracked_product.uom_id
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
mo.qty_producing = 2
|
||||
mo.move_raw_ids.move_line_ids.write({'qty_done': 1})
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
# create a Mo to produce 2 units of tracked product
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = tracked_product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 2
|
||||
mo_form.product_uom_id = tracked_product.uom_id
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
|
||||
mo.action_assign()
|
||||
# Open the wizard
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
# Let the wizard generate all serial numbers
|
||||
wizard.next_serial_number = "sn#3"
|
||||
wizard.next_serial_count = 2
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to apply generated serial numbers
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().apply()
|
||||
# Initial MO should have a backorder-sequenced name and be in to_close state
|
||||
self.assertTrue("-001" in mo.name)
|
||||
self.assertEqual(mo.state, "to_close")
|
||||
# Each generated serial number should have its own mo
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 2)
|
||||
# Check generated serial numbers
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.lot_producing_id.mapped('name'), ["sn#3", "sn#4"])
|
||||
#check byproduct quantity
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.move_byproduct_ids.mapped('quantity_done'), [1, 1])
|
||||
# check the component quantity
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.move_raw_ids.mapped('quantity_done'), [1, 1])
|
||||
# Mark the MOs as done
|
||||
mo.procurement_group_id.mrp_production_ids.button_mark_done()
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped('state'), ['done', 'done'])
|
||||
|
||||
def test_smp_produce_with_consumable_component(self):
|
||||
"""Create a MO for a product tracked by serial number with a consumable component.
|
||||
Open the smp wizard, You should be able to generate all serial numbers.
|
||||
BoM:
|
||||
- 1x final product (tracked by serial number)
|
||||
components:
|
||||
- 2 x (storable)
|
||||
- 4 x (consumable)
|
||||
- Create a MO with 12 final products to produce.
|
||||
- update the component quantity to 100
|
||||
"""
|
||||
self.bom_1.product_id.uom_id = self.ref('uom.product_uom_unit')
|
||||
self.bom_1.product_id.tracking = 'serial'
|
||||
self.bom_1.product_qty = 1
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.bom_1
|
||||
mo_form.product_qty = 12
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(mo.move_raw_ids.mapped(lambda l: l.product_qty), [24, 48])
|
||||
self.assertEqual(mo.move_raw_ids[1].product_id.type, 'consu', 'The second component should be consumable')
|
||||
mo.move_raw_ids[1].product_uom_qty = 100
|
||||
|
||||
# Make some stock and reserve for storable component
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': mo.move_raw_ids[0].product_id.id,
|
||||
'inventory_quantity': 24,
|
||||
'location_id': mo.location_src_id.id,
|
||||
})._apply_inventory()
|
||||
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
|
||||
# Open the wizard
|
||||
action = mo.action_serial_mass_produce_wizard()
|
||||
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
|
||||
# Let the wizard generate all serial numbers
|
||||
wizard.next_serial_number = "sn#1"
|
||||
wizard.next_serial_count = mo.product_qty
|
||||
action = wizard.save().generate_serial_numbers_production()
|
||||
# Reload the wizard to apply generated serial numbers
|
||||
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
|
||||
wizard.save().apply()
|
||||
# Initial MO should have a backorder-sequenced name and be in to_close state
|
||||
self.assertTrue("-001" in mo.name)
|
||||
self.assertEqual(mo.state, "to_close")
|
||||
# Each generated serial number should have its own mo
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 12)
|
||||
|
|
@ -12,8 +12,20 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
unit = cls.env.ref("uom.product_uom_unit")
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
cls.graphics_card = cls.env['product.product'].create({
|
||||
'name': 'Individual Workplace',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'type': 'consu',
|
||||
'is_storable': True,
|
||||
'tracking': 'none',
|
||||
})
|
||||
cls.laptop = cls.env['product.product'].create({
|
||||
'name': 'Acoustic Bloc Screens',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'type': 'consu',
|
||||
'is_storable': True,
|
||||
'tracking': 'none',
|
||||
})
|
||||
cls.depot_location = cls.env['stock.location'].create({
|
||||
'name': 'Depot',
|
||||
'usage': 'internal',
|
||||
|
|
@ -22,14 +34,13 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
cls.env["stock.putaway.rule"].create({
|
||||
"location_in_id": cls.stock_location.id,
|
||||
"location_out_id": cls.depot_location.id,
|
||||
'category_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.env['stock.quant'].create({
|
||||
'location_id': cls.stock_location_14.id,
|
||||
'location_id': cls.shelf_1.id,
|
||||
'product_id': cls.graphics_card.id,
|
||||
'inventory_quantity': 16.0
|
||||
}).action_apply_inventory()
|
||||
|
|
@ -37,16 +48,19 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
cls.bom_laptop = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.laptop.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'bom_line_ids': [Command.create({
|
||||
'product_id': cls.graphics_card.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit.id
|
||||
'product_uom_id': cls.uom_unit.id
|
||||
})],
|
||||
'operation_ids': [Command.create({
|
||||
'name': 'Cutting Machine',
|
||||
'workcenter_id': cls.workcenter_1.id,
|
||||
'time_cycle': 12,
|
||||
'sequence': 1,
|
||||
})],
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
def new_mo_laptop(self):
|
||||
|
|
@ -67,22 +81,114 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
self.assertEqual(self.warehouse_1.manufacture_pull_id, manu_rule)
|
||||
manu_route = manu_rule.route_id
|
||||
self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
|
||||
warehouse_1_stock_manager.write({
|
||||
'manufacture_to_resupply': False
|
||||
})
|
||||
warehouse_1_stock_manager.manufacture_to_resupply = False
|
||||
self.assertFalse(self.warehouse_1.manufacture_pull_id.active)
|
||||
self.assertFalse(self.warehouse_1.manu_type_id.active)
|
||||
self.assertFalse(self.picking_type_manu.active)
|
||||
self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes())
|
||||
warehouse_1_stock_manager.write({
|
||||
'manufacture_to_resupply': True
|
||||
})
|
||||
warehouse_1_stock_manager.manufacture_to_resupply = True
|
||||
manu_rule = self.env['stock.rule'].search([
|
||||
('action', '=', 'manufacture'),
|
||||
('warehouse_id', '=', self.warehouse_1.id)])
|
||||
self.assertEqual(self.warehouse_1.manufacture_pull_id, manu_rule)
|
||||
self.assertTrue(self.warehouse_1.manu_type_id.active)
|
||||
self.assertTrue(self.picking_type_manu.active)
|
||||
self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
|
||||
|
||||
def test_manufacturing_rule_other_dest(self):
|
||||
""" Ensures that a manufacturing rule can define a destination the rule itself and have it
|
||||
applied instead of the one from the operation type if location_dest_from_rule is set.
|
||||
"""
|
||||
freezer_loc = self.env['stock.location'].create({
|
||||
'name': 'Freezer',
|
||||
'location_id': self.warehouse_1.view_location_id.id,
|
||||
})
|
||||
route = self.env['stock.route'].create({
|
||||
'name': 'Manufacture then freeze',
|
||||
'rule_ids': [
|
||||
Command.create({
|
||||
'name': 'Freezer -> Stock',
|
||||
'action': 'pull',
|
||||
'procure_method': 'make_to_order',
|
||||
'picking_type_id': self.warehouse_1.int_type_id.id,
|
||||
'location_src_id': freezer_loc.id,
|
||||
'location_dest_id': self.warehouse_1.lot_stock_id.id,
|
||||
'location_dest_from_rule': True,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Manufacture',
|
||||
'action': 'manufacture',
|
||||
'picking_type_id': self.warehouse_1.manu_type_id.id,
|
||||
'location_src_id': self.warehouse_1.lot_stock_id.id,
|
||||
'location_dest_id': freezer_loc.id,
|
||||
'location_dest_from_rule': True,
|
||||
}),
|
||||
],
|
||||
})
|
||||
# Remove the classic Manufacture route if it exists and replace it by the new one
|
||||
self.product_4.route_ids = [
|
||||
Command.link(route.id),
|
||||
Command.unlink(self.warehouse_1.manufacture_pull_id.id),
|
||||
]
|
||||
|
||||
# Create a procurement to resupply the Stock, taking from the Freezer.
|
||||
self.env['stock.rule'].run([
|
||||
self.env['stock.rule'].Procurement(
|
||||
self.product_4,
|
||||
5.0,
|
||||
self.product_4.uom_id,
|
||||
self.warehouse_1.lot_stock_id,
|
||||
'test_other_dest',
|
||||
'test_other_dest',
|
||||
self.warehouse_1.company_id,
|
||||
{
|
||||
'warehouse_id': self.warehouse_1,
|
||||
}
|
||||
)
|
||||
])
|
||||
|
||||
# Make sure the production is delivering the goods in the location set on the rule.
|
||||
production = self.env['mrp.production'].search([('product_id', '=', self.product_4.id)])
|
||||
self.assertEqual(len(production), 1)
|
||||
self.assertEqual(production.picking_type_id.default_location_dest_id, self.warehouse_1.lot_stock_id)
|
||||
self.assertEqual(production.location_dest_id, freezer_loc)
|
||||
|
||||
def test_multi_warehouse_resupply(self):
|
||||
""" test a multi warehouse flow give a correct date delay
|
||||
product_6 is sold from warehouse_1, its component (product_4) is
|
||||
resupplied from warehouse_2 and manufactured in warehouse_2.
|
||||
Everything in mto """
|
||||
|
||||
self.route_mto.active = True
|
||||
warehouse_2 = self.env['stock.warehouse'].create({
|
||||
'name': 'Warehouse 2',
|
||||
'code': 'WH2',
|
||||
})
|
||||
# product 4 can only be manufacture in WH2
|
||||
self.bom_1.picking_type_id = warehouse_2.manu_type_id
|
||||
|
||||
self.warehouse_1.manufacture_steps = "pbm"
|
||||
self.warehouse_1.resupply_wh_ids = [Command.set([warehouse_2.id])]
|
||||
self.product_6.route_ids = [Command.set([self.route_manufacture.id, self.route_mto.id])]
|
||||
self.product_4.route_ids = [Command.set([
|
||||
self.warehouse_1.resupply_route_ids.id,
|
||||
self.route_mto.id,
|
||||
])]
|
||||
warehouse_2.resupply_route_ids.rule_ids.procure_method = 'make_to_order'
|
||||
|
||||
self.env['stock.rule'].run([
|
||||
self.env['stock.rule'].Procurement(
|
||||
self.product_6,
|
||||
5.0,
|
||||
self.product_6.uom_id,
|
||||
self.customer_location,
|
||||
'test_ressuply',
|
||||
'test_ressuply',
|
||||
self.warehouse_1.company_id,
|
||||
{
|
||||
'warehouse_id': self.warehouse_1,
|
||||
},
|
||||
),
|
||||
])
|
||||
|
||||
def test_manufacturing_scrap(self):
|
||||
"""
|
||||
Testing to do a scrap of consumed material.
|
||||
|
|
@ -100,17 +206,15 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
lot_product_4 = self.env['stock.lot'].create({
|
||||
'name': '0000000000001',
|
||||
'product_id': self.product_4.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_product_2 = self.env['stock.lot'].create({
|
||||
'name': '0000000000002',
|
||||
'product_id': self.product_2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
# Inventory for Stick
|
||||
self.env['stock.quant'].create({
|
||||
'location_id': self.stock_location_14.id,
|
||||
'location_id': self.shelf_1.id,
|
||||
'product_id': self.product_4.id,
|
||||
'inventory_quantity': 8,
|
||||
'lot_id': lot_product_4.id
|
||||
|
|
@ -118,7 +222,7 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
|
||||
# Inventory for Stone Tools
|
||||
self.env['stock.quant'].create({
|
||||
'location_id': self.stock_location_14.id,
|
||||
'location_id': self.shelf_1.id,
|
||||
'product_id': self.product_2.id,
|
||||
'inventory_quantity': 12,
|
||||
'lot_id': lot_product_2.id
|
||||
|
|
@ -147,17 +251,17 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
# Scrap Product Wood with lot.
|
||||
scrap_id = self.env['stock.scrap'].with_context(active_model='mrp.production', active_id=production_3.id).create({'product_id': self.product_2.id, 'scrap_qty': 1.0, 'product_uom_id': self.product_2.uom_id.id, 'location_id': location_id, 'lot_id': lot_product_2.id, 'production_id': production_3.id})
|
||||
scrap_id.do_scrap()
|
||||
scrap_move = scrap_id.move_id
|
||||
scrap_move = scrap_id.move_ids[0]
|
||||
|
||||
self.assertTrue(scrap_move.raw_material_production_id)
|
||||
self.assertTrue(scrap_move.scrapped)
|
||||
self.assertEqual(scrap_move.location_dest_usage, 'inventory')
|
||||
self.assertEqual(scrap_move.location_dest_id, scrap_id.scrap_location_id)
|
||||
self.assertEqual(scrap_move.price_unit, scrap_move.product_id.standard_price)
|
||||
|
||||
#Check scrap move is created for this production order.
|
||||
#TODO: should check with scrap objects link in between
|
||||
|
||||
# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.scrapped)
|
||||
# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.location_dest_usage == 'inventory')
|
||||
# self.assertTrue(scrap_move, "There are no any scrap move created for production order.")
|
||||
|
||||
def test_putaway_after_manufacturing_3(self):
|
||||
|
|
@ -167,11 +271,10 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
"""
|
||||
self.laptop.tracking = 'serial'
|
||||
mo_laptop = self.new_mo_laptop()
|
||||
serial = self.env['stock.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id})
|
||||
serial = self.env['stock.lot'].create({'product_id': self.laptop.id})
|
||||
|
||||
mo_form = Form(mo_laptop)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.lot_producing_id = serial
|
||||
mo_form.lot_producing_ids.set(serial)
|
||||
mo_laptop = mo_form.save()
|
||||
mo_laptop.button_mark_done()
|
||||
|
||||
|
|
@ -183,10 +286,9 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
|
||||
def test_backorder_unpacking(self):
|
||||
""" Test that movement of pack in backorder is correctly handled. """
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
warehouse.write({'manufacture_steps': 'pbm'})
|
||||
self.warehouse_1.manufacture_steps = 'pbm'
|
||||
|
||||
self.product_1.type = 'product'
|
||||
self.product_1.is_storable = True
|
||||
self.env['stock.quant']._update_available_quantity(self.product_1, self.stock_location, 100)
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -195,25 +297,30 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
package = self.env['stock.quant.package'].create({})
|
||||
package = self.env['stock.package'].create({})
|
||||
|
||||
picking = mo.picking_ids
|
||||
picking.move_line_ids.write({
|
||||
'qty_done': 20,
|
||||
'quantity': 20,
|
||||
'result_package_id': package.id,
|
||||
})
|
||||
|
||||
res_dict = picking.button_validate()
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.process()
|
||||
Form.from_action(self.env, picking.button_validate()).save().process()
|
||||
|
||||
backorder = picking.backorder_ids
|
||||
backorder.move_line_ids.qty_done = 80
|
||||
backorder.move_line_ids.quantity = 80
|
||||
backorder.button_validate()
|
||||
|
||||
self.assertEqual(picking.state, 'done')
|
||||
self.assertEqual(backorder.state, 'done')
|
||||
self.assertEqual(mo.move_raw_ids.move_line_ids.mapped('reserved_qty'), [20, 80])
|
||||
self.assertEqual(mo.move_raw_ids.move_line_ids.mapped('quantity_product_uom'), [20, 80])
|
||||
|
||||
def test_produce_with_zero_available_qty(self):
|
||||
""" Test that producing with 0 qty_available for the component
|
||||
still links the stock.move.line to the production order. """
|
||||
mo, *_ = self.generate_mo()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.move_raw_ids.move_line_ids.production_id, mo)
|
||||
|
||||
def test_unarchive_mto_route_active_needed_rules_only(self):
|
||||
""" Ensure that activating a route will activate only its relevant rules.
|
||||
|
|
@ -221,33 +328,32 @@ class TestWarehouseMrp(common.TestMrpCommon):
|
|||
location if manufacture is in 1 step since this location is archived.
|
||||
"""
|
||||
|
||||
self.env.user.groups_id += self.env.ref('stock.group_adv_location')
|
||||
mto_route = self.env.ref('stock.route_warehouse0_mto')
|
||||
self.env.user.group_ids += self.env.ref('stock.group_adv_location')
|
||||
|
||||
# initially 'WH: Stock → Pre-Production (MTO)' is inactive and not shown in MTO route.
|
||||
self.assertEqual(self.warehouse_1.manufacture_steps, 'mrp_one_step')
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
|
||||
self.assertFalse(mto_route.active)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
|
||||
self.assertFalse(self.route_mto.active)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, self.route_mto.rule_ids)
|
||||
|
||||
# Activate the MTO route and still 'WH: Stock → Pre-Production (MTO)' is not shown in MTO route.
|
||||
mto_route.active = True
|
||||
self.route_mto.active = True
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, self.route_mto.rule_ids)
|
||||
|
||||
# Change MRP steps mrp_one_step to pbm_sam and now that rule is shown in mto route.
|
||||
self.warehouse_1.manufacture_steps = 'pbm_sam'
|
||||
self.assertTrue(self.warehouse_1.pbm_mto_pull_id.active)
|
||||
self.assertTrue(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
|
||||
self.assertIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
|
||||
self.assertIn(self.warehouse_1.pbm_mto_pull_id, self.route_mto.rule_ids)
|
||||
|
||||
# Revert to mrp_one_step MRP and confirm rules visibility is updated correctly
|
||||
self.warehouse_1.manufacture_steps = 'mrp_one_step'
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.active)
|
||||
self.assertFalse(self.warehouse_1.pbm_mto_pull_id.location_dest_id.active)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, mto_route.rule_ids)
|
||||
self.assertNotIn(self.warehouse_1.pbm_mto_pull_id, self.route_mto.rule_ids)
|
||||
|
||||
|
||||
class TestKitPicking(common.TestMrpCommon):
|
||||
|
|
@ -258,7 +364,7 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
def create_product(name):
|
||||
p = Form(cls.env['product.product'])
|
||||
p.name = name
|
||||
p.detailed_type = 'product'
|
||||
p.is_storable = True
|
||||
return p.save()
|
||||
|
||||
# Create a kit 'kit_parent' :
|
||||
|
|
@ -282,13 +388,13 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
component_f = create_product('Comp F')
|
||||
component_g = create_product('Comp G')
|
||||
# Creating all kits
|
||||
kit_1 = create_product('Kit 1')
|
||||
kit_2 = create_product('Kit 2')
|
||||
kit_3 = create_product('kit 3')
|
||||
cls.kit_1 = create_product('Kit 1')
|
||||
cls.kit_2 = create_product('Kit 2')
|
||||
cls.kit_3 = create_product('kit 3')
|
||||
cls.kit_parent = create_product('Kit Parent')
|
||||
# Linking the kits and the components via some 'phantom' BoMs
|
||||
bom_kit_1 = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_1.product_tmpl_id.id,
|
||||
'product_tmpl_id': cls.kit_1.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
BomLine = cls.env['mrp.bom.line']
|
||||
|
|
@ -305,7 +411,7 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
'product_qty': 3.0,
|
||||
'bom_id': bom_kit_1.id})
|
||||
bom_kit_2 = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_2.product_tmpl_id.id,
|
||||
'product_tmpl_id': cls.kit_2.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
BomLine.create({
|
||||
|
|
@ -313,7 +419,7 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_2.id})
|
||||
BomLine.create({
|
||||
'product_id': kit_1.id,
|
||||
'product_id': cls.kit_1.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_2.id})
|
||||
bom_kit_parent = cls.env['mrp.bom'].create({
|
||||
|
|
@ -325,11 +431,11 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
BomLine.create({
|
||||
'product_id': kit_2.id,
|
||||
'product_id': cls.kit_2.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
bom_kit_3 = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_3.product_tmpl_id.id,
|
||||
'product_tmpl_id': cls.kit_3.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
BomLine.create({
|
||||
|
|
@ -341,7 +447,7 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_3.id})
|
||||
BomLine.create({
|
||||
'product_id': kit_3.id,
|
||||
'product_id': cls.kit_3.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
|
||||
|
|
@ -352,7 +458,7 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
cls.test_supplier = cls.env['stock.location'].create({
|
||||
'name': 'supplier',
|
||||
'usage': 'supplier',
|
||||
'location_id': cls.env.ref('stock.stock_location_stock').id,
|
||||
'location_id': cls.stock_location.id,
|
||||
})
|
||||
|
||||
cls.expected_quantities = {
|
||||
|
|
@ -366,23 +472,22 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
}
|
||||
|
||||
def test_kit_immediate_transfer(self):
|
||||
""" Make sure a kit is split in the corrects quantity_done by components in case of an
|
||||
""" Make sure a kit is split in the corrects quantity by components in case of an
|
||||
immediate transfer.
|
||||
"""
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.test_supplier.id,
|
||||
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
|
||||
'partner_id': self.test_partner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
'immediate_transfer': True
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
})
|
||||
move_receipt_1 = self.env['stock.move'].create({
|
||||
'name': self.kit_parent.name,
|
||||
self.env['stock.move'].create({
|
||||
'product_id': self.kit_parent.id,
|
||||
'quantity_done': 3,
|
||||
'quantity': 3,
|
||||
'picked': True,
|
||||
'product_uom': self.kit_parent.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
'location_id': self.test_supplier.id,
|
||||
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
|
||||
})
|
||||
|
|
@ -390,8 +495,9 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
|
||||
# We check that the picking has the correct quantities after its move were splitted.
|
||||
self.assertEqual(len(picking.move_ids), 7)
|
||||
for move_line in picking.move_ids:
|
||||
self.assertEqual(move_line.quantity_done, self.expected_quantities[move_line.product_id])
|
||||
for move in picking.move_ids:
|
||||
self.assertEqual(move.quantity, self.expected_quantities[move.product_id])
|
||||
self.assertEqual(move.state, 'done')
|
||||
|
||||
def test_kit_planned_transfer(self):
|
||||
""" Make sure a kit is split in the corrects product_qty by components in case of a
|
||||
|
|
@ -401,16 +507,14 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
'location_id': self.test_supplier.id,
|
||||
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
|
||||
'partner_id': self.test_partner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
'immediate_transfer': False,
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
})
|
||||
move_receipt_1 = self.env['stock.move'].create({
|
||||
'name': self.kit_parent.name,
|
||||
'product_id': self.kit_parent.id,
|
||||
'product_uom_qty': 3,
|
||||
'product_uom': self.kit_parent.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
'location_id': self.test_supplier.id,
|
||||
'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
|
||||
})
|
||||
|
|
@ -423,56 +527,53 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
|
||||
def test_add_sml_with_kit_to_confirmed_picking(self):
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
customer_location = self.env.ref('stock.stock_location_customers')
|
||||
stock_location = warehouse.lot_stock_id
|
||||
in_type = warehouse.in_type_id
|
||||
|
||||
self.bom_4.type = 'phantom'
|
||||
kit = self.bom_4.product_id
|
||||
compo = self.bom_4.bom_line_ids.product_id
|
||||
product = self.env['product.product'].create({'name': 'Super Product', 'type': 'product'})
|
||||
product = self.env['product.product'].create({'name': 'Super Product', 'is_storable': True})
|
||||
|
||||
receipt = self.env['stock.picking'].create({
|
||||
'picking_type_id': in_type.id,
|
||||
'location_id': customer_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'move_ids': [(0, 0, {
|
||||
'name': product.name,
|
||||
'location_id': self.customer_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'move_ids': [Command.create({
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'location_id': customer_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'location_id': self.customer_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
})]
|
||||
})
|
||||
receipt.action_confirm()
|
||||
|
||||
receipt.move_line_ids.qty_done = 1
|
||||
receipt.move_line_ids = [(0, 0, {
|
||||
receipt.move_line_ids.quantity = 1
|
||||
receipt.move_line_ids = [Command.create({
|
||||
'product_id': kit.id,
|
||||
'qty_done': 1,
|
||||
'quantity': 1,
|
||||
'product_uom_id': kit.uom_id.id,
|
||||
'location_id': customer_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'location_id': self.customer_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
})]
|
||||
receipt.move_ids.picked = True
|
||||
|
||||
receipt.button_validate()
|
||||
|
||||
self.assertEqual(receipt.state, 'done')
|
||||
self.assertRecordValues(receipt.move_ids, [
|
||||
{'product_id': product.id, 'quantity_done': 1, 'state': 'done'},
|
||||
{'product_id': compo.id, 'quantity_done': 1, 'state': 'done'},
|
||||
{'product_id': product.id, 'quantity': 1, 'state': 'done'},
|
||||
{'product_id': compo.id, 'quantity': 1, 'state': 'done'},
|
||||
])
|
||||
|
||||
def test_move_line_aggregated_product_quantities_with_kit(self):
|
||||
""" Test the `stock.move.line` method `_get_aggregated_product_quantities`,
|
||||
who returns data used to print delivery slips, using kits.
|
||||
"""
|
||||
uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
kit, kit_component_1, kit_component_2, not_kit_1, not_kit_2 = self.env['product.product'].create([{
|
||||
'name': name,
|
||||
'type': 'product',
|
||||
'uom_id': uom_unit.id,
|
||||
'is_storable': True,
|
||||
'uom_id': self.uom_unit.id,
|
||||
} for name in ['Kit', 'Kit Component 1', 'Kit Component 2', 'Not Kit 1', 'Not Kit 2']])
|
||||
|
||||
bom_kit = self.env['mrp.bom'].create({
|
||||
|
|
@ -494,25 +595,25 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
})
|
||||
|
||||
delivery_form = Form(self.env['stock.picking'])
|
||||
delivery_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
delivery_form.picking_type_id = self.picking_type_in
|
||||
with delivery_form.move_ids.new() as move:
|
||||
move.product_id = bom_kit.product_id
|
||||
move.product_uom_qty = 4
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
with delivery_form.move_ids.new() as move:
|
||||
move.product_id = not_kit_1
|
||||
move.product_uom_qty = 4
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
with delivery_form.move_ids.new() as move:
|
||||
move.product_id = not_kit_2
|
||||
move.product_uom_qty = 3
|
||||
delivery = delivery_form.save()
|
||||
delivery.action_confirm()
|
||||
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_1).qty_done = 3
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_2).qty_done = 3
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_1).qty_done = 4
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_2).qty_done = 2
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_1).quantity = 3
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == kit_component_2).quantity = 3
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_1).quantity = 4
|
||||
delivery.move_line_ids.filtered(lambda ml: ml.product_id == not_kit_2).quantity = 2
|
||||
backorder_wizard_dict = delivery.button_validate()
|
||||
backorder_wizard_form = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context']))
|
||||
backorder_wizard_form = Form.from_action(self.env, backorder_wizard_dict)
|
||||
backorder_wizard_form.save().process_cancel_backorder()
|
||||
|
||||
aggregate_not_kit_values = delivery.move_line_ids._get_aggregated_product_quantities()
|
||||
|
|
@ -522,3 +623,139 @@ class TestKitPicking(common.TestMrpCommon):
|
|||
aggregate_kit_values = delivery.move_line_ids._get_aggregated_product_quantities(kit_name=bom_kit.product_id.name)
|
||||
self.assertEqual(len(aggregate_kit_values.keys()), 2)
|
||||
self.assertTrue(all('Component' in val for val in aggregate_kit_values), 'Only kit products should be included')
|
||||
|
||||
def test_scrap_consu_kit_not_available(self):
|
||||
"""
|
||||
Scrap a consumable kit with one product not available in stock
|
||||
"""
|
||||
self._test_scrap_kit_not_available(False)
|
||||
|
||||
def test_scrap_storable_kit_not_available(self):
|
||||
"""
|
||||
Scrap a storable kit with one product not available in stock
|
||||
"""
|
||||
self._test_scrap_kit_not_available(True)
|
||||
|
||||
def _test_scrap_kit_not_available(self, storable):
|
||||
bom = self.bom_4
|
||||
bom.type = 'phantom'
|
||||
|
||||
kit = bom.product_id
|
||||
component = bom.bom_line_ids.product_id
|
||||
kit.is_storable = storable
|
||||
component.is_storable = True
|
||||
|
||||
scrap = self.env['stock.scrap'].create({
|
||||
'product_id': kit.id,
|
||||
'product_uom_id': kit.uom_id.id,
|
||||
'scrap_qty': 1,
|
||||
'bom_id': bom.id,
|
||||
})
|
||||
|
||||
Form.from_action(self.env, scrap.action_validate()).save().action_done()
|
||||
|
||||
self.assertEqual(scrap.state, 'done')
|
||||
self.assertRecordValues(scrap.move_ids, [
|
||||
{'product_id': component.id, 'quantity': 1, 'state': 'done'}
|
||||
])
|
||||
|
||||
def test_kit_with_packaging_different_uom(self):
|
||||
"""
|
||||
Test that a quantity packaging is correctly computed on a move line
|
||||
when a kit is in a different uom than its components.
|
||||
- Component(uom=Kg)
|
||||
- Kit (uom=unit) -> Bom (1 dozen) -> Component (10 g)
|
||||
- Packaging (qty=2 units of kit)
|
||||
"""
|
||||
bom = self.bom_4
|
||||
bom.product_id = False
|
||||
bom.type = 'phantom'
|
||||
kit = bom.product_tmpl_id.product_variant_id
|
||||
kit.is_storable = True
|
||||
# product is in unit and bom in dozen
|
||||
kit.uom_id = self.uom_unit
|
||||
bom.product_uom_id = self.uom_dozen
|
||||
bom.product_qty = 1
|
||||
# create a packaging with 2 units
|
||||
packaging = self.env['uom.uom'].create({
|
||||
'name': 'Pack of 2',
|
||||
'relative_factor': 2,
|
||||
'relative_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
})
|
||||
# component is in Kg but bom_line in gram
|
||||
component = bom.bom_line_ids.product_id
|
||||
component.uom_id = self.uom_kg
|
||||
bom.bom_line_ids.product_uom_id = self.uom_gram
|
||||
bom.bom_line_ids.product_qty = 10
|
||||
|
||||
# create a delivery with 20 units of kit
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
stock_location = warehouse.lot_stock_id
|
||||
|
||||
delivery = self.env['stock.picking'].create({
|
||||
'picking_type_id': self.picking_type_out.id,
|
||||
'location_id': stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'move_ids': [Command.create({
|
||||
'product_id': kit.id,
|
||||
'product_uom_qty': 12,
|
||||
'product_uom': packaging.id,
|
||||
'location_id': stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
})],
|
||||
})
|
||||
delivery.action_confirm()
|
||||
self.assertEqual(delivery.move_ids.product_id, component)
|
||||
self.assertEqual(delivery.move_ids.product_uom_qty, 20)
|
||||
delivery.move_ids.quantity = 20
|
||||
delivery.move_ids.picked = True
|
||||
delivery.button_validate()
|
||||
self.assertTrue(delivery.state, 'done')
|
||||
|
||||
def test_search_kit_on_quantity(self):
|
||||
self.env['stock.quant'].create([{
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': qty,
|
||||
'location_id': self.test_supplier.id,
|
||||
} for product, qty in self.expected_quantities.items()]).action_apply_inventory()
|
||||
|
||||
products = self.env['product.product'].search([
|
||||
'&', ('qty_available', '>', 3), ('qty_available', '<', 9),
|
||||
])
|
||||
self.assertNotIn(self.kit_1, products) # 12
|
||||
self.assertIn(self.kit_2, products) # 6
|
||||
self.assertNotIn(self.kit_3, products) # 3
|
||||
|
||||
def test_scrap_change_product(self):
|
||||
""" Ensure a scrap order automatically updates the BoM when its product is changed,
|
||||
selecting the product's first BoM if it's a kit or set the field empty otherwise."""
|
||||
bom_a = self.bom_1
|
||||
bom_a.type = 'phantom'
|
||||
product_a = bom_a.product_id
|
||||
|
||||
bom_b = self.bom_3
|
||||
bom_b.type = 'phantom'
|
||||
product_b = bom_b.product_id
|
||||
|
||||
product_c = self.env['product.product'].create({'name': 'product_c', 'is_storable': True})
|
||||
|
||||
form = Form(self.env['stock.scrap'])
|
||||
form.product_id = product_a
|
||||
form.bom_id = bom_a
|
||||
form.scrap_qty = 1
|
||||
scrap = form.save()
|
||||
|
||||
# assert the scrap's bom_id is set to bom_a
|
||||
self.assertEqual(scrap.bom_id, bom_a)
|
||||
|
||||
form.product_id = product_b
|
||||
scrap = form.save()
|
||||
|
||||
# assert the scrap's bom_id is set to bom_b after updating the product
|
||||
self.assertEqual(scrap.bom_id, bom_b)
|
||||
|
||||
form.product_id = product_c
|
||||
scrap = form.save()
|
||||
|
||||
# assert the scrap's bom_id is updated to False after updating the product
|
||||
self.assertFalse(scrap.bom_id)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.stock.tests.test_report import TestReportsCommon
|
||||
from odoo import Command
|
||||
|
||||
|
||||
class TestMrpStockReports(TestReportsCommon):
|
||||
|
|
@ -14,14 +15,19 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
product_chocolate = self.env['product.product'].create({
|
||||
'name': 'Chocolate',
|
||||
'type': 'consu',
|
||||
'standard_price': 10
|
||||
})
|
||||
product_chococake = self.env['product.product'].create({
|
||||
'name': 'Choco Cake',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
product_double_chococake = self.env['product.product'].create({
|
||||
'name': 'Double Choco Cake',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
byproduct = self.env['product.product'].create({
|
||||
'name': 'by-product',
|
||||
'is_storable': True,
|
||||
})
|
||||
|
||||
# Creates two BOM: one creating a regular slime, one using regular slimes.
|
||||
|
|
@ -34,6 +40,8 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_chocolate.id, 'product_qty': 4}),
|
||||
],
|
||||
'byproduct_ids':
|
||||
[(0, 0, {'product_id': byproduct.id, 'product_qty': 2, 'cost_share': 1.8})],
|
||||
})
|
||||
bom_double_chococake = self.env['mrp.bom'].create({
|
||||
'product_id': product_double_chococake.id,
|
||||
|
|
@ -59,9 +67,9 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
mo_2 = mo_form.save()
|
||||
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids)
|
||||
draft_picking_qty = docs['draft_picking_qty']
|
||||
draft_production_qty = docs['draft_production_qty']
|
||||
self.assertEqual(len(lines), 0, "Must have 0 line.")
|
||||
draft_picking_qty = self.sum_dicts(docs['product'], 'draft_picking_qty')
|
||||
draft_production_qty = self.sum_dicts(docs['product'], 'draft_production_qty')
|
||||
self.assertEqual(len(lines), 1, "Must have 1 line.")
|
||||
self.assertEqual(draft_picking_qty['in'], 0)
|
||||
self.assertEqual(draft_picking_qty['out'], 0)
|
||||
self.assertEqual(draft_production_qty['in'], 10)
|
||||
|
|
@ -71,15 +79,15 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
mo_1.action_confirm()
|
||||
mo_2.action_confirm()
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids)
|
||||
draft_picking_qty = docs['draft_picking_qty']
|
||||
draft_production_qty = docs['draft_production_qty']
|
||||
draft_picking_qty = self.sum_dicts(docs['product'], 'draft_picking_qty')
|
||||
draft_production_qty = self.sum_dicts(docs['product'], 'draft_production_qty')
|
||||
self.assertEqual(len(lines), 2, "Must have two line.")
|
||||
line_1 = lines[0]
|
||||
line_2 = lines[1]
|
||||
self.assertEqual(line_1['document_in'].id, mo_1.id)
|
||||
self.assertEqual(line_1['document_in']['id'], mo_1.id)
|
||||
self.assertEqual(line_1['quantity'], 4)
|
||||
self.assertEqual(line_1['document_out'].id, mo_2.id)
|
||||
self.assertEqual(line_2['document_in'].id, mo_1.id)
|
||||
self.assertEqual(line_1['document_out']['id'], mo_2.id)
|
||||
self.assertEqual(line_2['document_in']['id'], mo_1.id)
|
||||
self.assertEqual(line_2['quantity'], 6)
|
||||
self.assertEqual(line_2['document_out'], False)
|
||||
self.assertEqual(draft_picking_qty['in'], 0)
|
||||
|
|
@ -87,6 +95,25 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
self.assertEqual(draft_production_qty['in'], 0)
|
||||
self.assertEqual(draft_production_qty['out'], 0)
|
||||
|
||||
mo_form = Form(mo_1)
|
||||
mo_form.qty_producing = 10
|
||||
mo_form.save()
|
||||
mo_1.move_byproduct_ids.quantity = 18
|
||||
mo_1.button_mark_done()
|
||||
|
||||
self.env.flush_all() # flush to correctly build report
|
||||
report_values = self.env['report.mrp.report_mo_overview']._get_report_data(mo_1.id)
|
||||
self.assertEqual(report_values['byproducts']['details'][0]['name'], byproduct.name)
|
||||
self.assertEqual(report_values['byproducts']['details'][0]['quantity'], 18)
|
||||
# (Component price $10) * (4 unit to produce one finished) * (the mo qty = 10 units) = $400
|
||||
self.assertEqual(report_values['components'][0]['summary']['mo_cost'], 400)
|
||||
# cost_share of byproduct = 1.8 -> 1.8 / 100 -> 0.018 * 400 = 7.2
|
||||
self.assertAlmostEqual(report_values['byproducts']['summary']['mo_cost'], 7.2)
|
||||
byproduct_report_values = report_values['cost_breakdown'][1]
|
||||
self.assertEqual(byproduct_report_values['name'], byproduct.name)
|
||||
# 7.2 / 18 units = 0.4
|
||||
self.assertAlmostEqual(byproduct_report_values['unit_avg_total_cost'], 0.4)
|
||||
|
||||
def test_report_forecast_2_production_backorder(self):
|
||||
""" Creates a manufacturing order and produces half the quantity.
|
||||
Then creates a backorder and checks the report.
|
||||
|
|
@ -97,7 +124,7 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
# Configures a product.
|
||||
product_apple_pie = self.env['product.product'].create({
|
||||
'name': 'Apple Pie',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
product_apple = self.env['product.product'].create({
|
||||
'name': 'Apple',
|
||||
|
|
@ -123,8 +150,9 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
pick = mo_1.move_raw_ids.move_orig_ids.picking_id
|
||||
pick.picking_type_id.show_operations = True # Could be false without demo data, as the lot group is disabled
|
||||
pick_form = Form(pick)
|
||||
with pick_form.move_line_ids_without_package.edit(0) as move_line:
|
||||
move_line.qty_done = 20
|
||||
with Form(pick.move_ids, view='stock.view_stock_move_operations') as form:
|
||||
with form.move_line_ids.edit(0) as move_line:
|
||||
move_line.quantity = 20
|
||||
pick = pick_form.save()
|
||||
pick.button_validate()
|
||||
# Produces 3 products then creates a backorder for the remaining product.
|
||||
|
|
@ -136,13 +164,13 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
backorder = backorder_form.save()
|
||||
backorder.action_backorder()
|
||||
|
||||
mo_2 = (mo_1.procurement_group_id.mrp_production_ids - mo_1)
|
||||
mo_2 = (mo_1.production_group_id.production_ids - mo_1)
|
||||
# Checks the forecast report.
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
|
||||
self.assertEqual(len(lines), 1, "Must have only one line about the backorder")
|
||||
self.assertEqual(lines[0]['document_in'].id, mo_2.id)
|
||||
self.assertEqual(lines[0]['quantity'], 1)
|
||||
self.assertEqual(lines[0]['document_out'], False)
|
||||
self.assertEqual(len(lines), 2, "Must have 2 lines, one about the backorder")
|
||||
self.assertEqual(lines[1]['document_in']['id'], mo_2.id)
|
||||
self.assertEqual(lines[1]['quantity'], 1)
|
||||
self.assertEqual(lines[1]['document_out'], False)
|
||||
|
||||
# Produces the last unit.
|
||||
mo_form = Form(mo_2)
|
||||
|
|
@ -151,14 +179,14 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
mo_2.button_mark_done()
|
||||
# Checks the forecast report.
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
|
||||
self.assertEqual(len(lines), 0, "Must have no line")
|
||||
self.assertEqual(len(lines), 1, "Free Stock line")
|
||||
|
||||
def test_report_forecast_3_report_line_corresponding_to_mo_highlighted(self):
|
||||
""" When accessing the report from a MO, checks if the correct MO is highlighted in the report
|
||||
"""
|
||||
product_banana = self.env['product.product'].create({
|
||||
'name': 'Banana',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
product_chocolate = self.env['product.product'].create({
|
||||
'name': 'Chocolate',
|
||||
|
|
@ -180,12 +208,62 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
for mo in [mo_1, mo_2]:
|
||||
context = mo.action_product_forecast_report()['context']
|
||||
_, _, lines = self.get_report_forecast(product_template_ids=product_banana.product_tmpl_id.ids, context=context)
|
||||
for line in lines:
|
||||
if line['document_in'] == mo:
|
||||
for line in lines[1:]:
|
||||
if line['document_in']['id'] == mo.id:
|
||||
self.assertTrue(line['is_matched'], "The corresponding MO line should be matched in the forecast report.")
|
||||
else:
|
||||
self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the MO shoud not be matched.")
|
||||
|
||||
def test_kit_packaging_delivery_slip(self):
|
||||
superkit = self.env['product.product'].create({
|
||||
'name': 'Super Kit',
|
||||
'type': 'consu',
|
||||
'uom_id': self.env['uom.uom'].create({
|
||||
'name': '6-pack',
|
||||
'relative_factor': 6,
|
||||
'relative_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
}).id,
|
||||
})
|
||||
|
||||
compo01, compo02 = self.env['product.product'].create([{
|
||||
'name': n,
|
||||
'is_storable': True,
|
||||
'uom_id': self.env.ref('uom.product_uom_meter').id,
|
||||
} for n in ['Compo 01', 'Compo 02']])
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': superkit.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'phantom',
|
||||
'product_uom_id': superkit.uom_id.id,
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': compo01.id, 'product_qty': 6}),
|
||||
(0, 0, {'product_id': compo02.id, 'product_qty': 6}),
|
||||
],
|
||||
})
|
||||
|
||||
for back_order, expected_vals in [('never', [12, 12]), ('always', [24, 12])]:
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.picking_type_in
|
||||
picking_form.partner_id = self.partner
|
||||
with picking_form.move_ids.new() as move:
|
||||
move.product_id = superkit
|
||||
move.product_uom_qty = 4
|
||||
picking = picking_form.save()
|
||||
picking.action_confirm()
|
||||
|
||||
picking.move_ids.write({'quantity': 12, 'picked': True})
|
||||
picking.picking_type_id.create_backorder = back_order
|
||||
picking.button_validate()
|
||||
non_kit_aggregate_values = picking.move_line_ids._get_aggregated_product_quantities()
|
||||
self.assertFalse(non_kit_aggregate_values)
|
||||
aggregate_values = picking.move_line_ids._get_aggregated_product_quantities(kit_name=superkit.display_name)
|
||||
for line in aggregate_values.values():
|
||||
self.assertItemsEqual([line[val] for val in ['qty_ordered', 'quantity']], expected_vals)
|
||||
|
||||
html_report = self.env['ir.actions.report']._render_qweb_html('stock.report_deliveryslip', picking.ids)[0]
|
||||
self.assertTrue(html_report, "report generated successfully")
|
||||
|
||||
def test_subkit_in_delivery_slip(self):
|
||||
"""
|
||||
Suppose this structure:
|
||||
|
|
@ -222,24 +300,24 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.picking_type_out
|
||||
picking_form.partner_id = self.partner
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
with picking_form.move_ids.new() as move:
|
||||
move.product_id = superkit
|
||||
move.product_uom_qty = 1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
with picking_form.move_ids.new() as move:
|
||||
move.product_id = subkit
|
||||
move.product_uom_qty = 1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
with picking_form.move_ids.new() as move:
|
||||
move.product_id = compo01
|
||||
move.product_uom_qty = 1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
with picking_form.move_ids.new() as move:
|
||||
move.product_id = compo02
|
||||
move.product_uom_qty = 1
|
||||
picking = picking_form.save()
|
||||
picking.action_confirm()
|
||||
|
||||
picking.move_ids.quantity_done = 1
|
||||
move = picking.move_ids.filtered(lambda m: m.name == "Super Kit" and m.product_id == compo03)
|
||||
move.move_line_ids.result_package_id = self.env['stock.quant.package'].create({'name': 'Package0001'})
|
||||
picking.move_ids.write({'quantity': 1, 'picked': True})
|
||||
move = picking.move_ids.filtered(lambda m: m.description_picking == "Super Kit" and m.product_id == compo03)
|
||||
move.move_line_ids.result_package_id = self.env['stock.package'].create({'name': 'Package0001'})
|
||||
picking.button_validate()
|
||||
|
||||
html_report = self.env['ir.actions.report']._render_qweb_html(
|
||||
|
|
@ -255,4 +333,409 @@ class TestMrpStockReports(TestReportsCommon):
|
|||
break
|
||||
if keys[0] in line:
|
||||
keys = keys[1:]
|
||||
|
||||
|
||||
self.assertFalse(keys, "All keys should be in the report with the defined order")
|
||||
|
||||
def test_mo_overview(self):
|
||||
""" Test that the overview does not traceback when the final produced qty is 0
|
||||
"""
|
||||
product_chocolate = self.env['product.product'].create({
|
||||
'name': 'Chocolate',
|
||||
'type': 'consu',
|
||||
})
|
||||
product_chococake = self.env['product.product'].create({
|
||||
'name': 'Choco Cake',
|
||||
'is_storable': True,
|
||||
})
|
||||
workcenter = self.env['mrp.workcenter'].create({
|
||||
'name': 'workcenter test',
|
||||
'costs_hour': 10,
|
||||
'time_start': 10,
|
||||
'time_stop': 10,
|
||||
'time_efficiency': 90,
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_chococake.id,
|
||||
'product_tmpl_id': product_chococake.product_tmpl_id.id,
|
||||
'product_uom_id': product_chococake.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_chocolate.id, 'product_qty': 4}),
|
||||
],
|
||||
'operation_ids': [
|
||||
Command.create({
|
||||
'name': 'Cutting Machine',
|
||||
'workcenter_id': workcenter.id,
|
||||
'time_cycle': 60,
|
||||
'sequence': 1
|
||||
}),
|
||||
],
|
||||
})
|
||||
mo = self.env['mrp.production'].create({
|
||||
'name': 'MO',
|
||||
'product_qty': 1.0,
|
||||
'product_id': product_chococake.id,
|
||||
})
|
||||
|
||||
mo.action_confirm()
|
||||
# check that the mo and bom cost are correctly calculated after mo confirmation
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(round(overview_values['data']['operations']['summary']['mo_cost'], 2), 14.45)
|
||||
self.assertEqual(round(overview_values['data']['operations']['summary']['bom_cost'], 2), 14.44)
|
||||
mo.button_mark_done()
|
||||
mo.qty_produced = 0.
|
||||
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(overview_values['data']['id'], mo.id, "computing overview value should work")
|
||||
|
||||
def test_overview_with_component_also_as_byproduct(self):
|
||||
""" Check that opening he overview of an MO for which the BoM contains an element as both component and byproduct
|
||||
does not cause an infinite recursion.
|
||||
"""
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.product.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_line_ids': [
|
||||
Command.create({'product_id': self.product1.id, 'product_qty': 1.0}),
|
||||
],
|
||||
'byproduct_ids': [
|
||||
Command.create({'product_id': self.product1.id, 'product_qty': 1.0})
|
||||
]
|
||||
})
|
||||
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': self.product.id,
|
||||
'bom_id': bom.id,
|
||||
'product_qty': 1,
|
||||
})
|
||||
|
||||
mo.action_confirm()
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(overview_values['data']['id'], mo.id, "Unexpected disparity between overview and MO data")
|
||||
|
||||
def test_multi_step_component_forecast_availability(self):
|
||||
"""
|
||||
Test that the component availability is correcly forecasted
|
||||
in multi step manufacturing
|
||||
"""
|
||||
# Configures the warehouse.
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
final_product, component = self.product, self.product1
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': final_product.id,
|
||||
'product_tmpl_id': final_product.product_tmpl_id.id,
|
||||
'product_uom_id': final_product.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
Command.create({'product_id': component.id, 'product_qty': 10}),
|
||||
],
|
||||
})
|
||||
# Creates a MO without any component in stock
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 2
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.components_availability, 'Not Available')
|
||||
self.assertEqual(mo.move_raw_ids.forecast_availability, -20.0)
|
||||
self.env['stock.quant']._update_available_quantity(component, warehouse.lot_stock_id, 100)
|
||||
# change the qty_producing to force a recompute of the availability
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 2.0
|
||||
self.assertEqual(mo.components_availability, 'Available')
|
||||
|
||||
def test_mo_overview_same_component(self):
|
||||
"""
|
||||
Test that for an mo for a product which has 2+ component lines for the same product,
|
||||
if there is some quantity of the component reserved, we properly match replenishments with
|
||||
components lines
|
||||
"""
|
||||
# BOM structure:
|
||||
# 'finished', manufactured:
|
||||
# - 1 'part'
|
||||
# - 1 'part'
|
||||
part, finished = self.env['product.product'].create([
|
||||
{
|
||||
'name': name,
|
||||
'type': 'consu',
|
||||
'is_storable': True,
|
||||
} for name in ['Part', 'Finished']
|
||||
])
|
||||
self.env['mrp.bom'].create([
|
||||
{
|
||||
'product_id': finished.id,
|
||||
'product_tmpl_id': finished.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
Command.create({'product_id': part.id, 'product_qty': 1.0})
|
||||
] * 2,
|
||||
}
|
||||
])
|
||||
# Put 2 parts in stock
|
||||
self.env['stock.quant']._update_available_quantity(part, self.stock_location, 2)
|
||||
|
||||
# Receive 20 parts
|
||||
self.env['stock.picking'].create({
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'move_type': 'one',
|
||||
'move_ids': [Command.create({
|
||||
'product_id': part.id,
|
||||
'product_uom_qty': 20,
|
||||
'location_id': self.env.ref('stock.stock_location_suppliers').id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
}),
|
||||
],
|
||||
}).action_confirm()
|
||||
|
||||
# Create an MO for 5 finished product
|
||||
mo = self.env['mrp.production'].create({
|
||||
'name': 'MO',
|
||||
'product_qty': 5.0,
|
||||
'product_id': finished.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
|
||||
# Test overview report values
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
[line0, line1] = overview_values['data']['components']
|
||||
[repl0, repl1] = line0['replenishments'], line1['replenishments']
|
||||
self.assertEqual(len(repl0), 1)
|
||||
self.assertEqual(len(repl1), 1)
|
||||
self.assertEqual(repl0[0]['summary']['quantity'], 3)
|
||||
self.assertEqual(repl1[0]['summary']['quantity'], 5)
|
||||
|
||||
def test_report_price_variants(self):
|
||||
"""
|
||||
This tests the MO's report price when a variant is involved. It makes sure
|
||||
that the BoM price takes only the current variant and not all of them. It also
|
||||
tests that the lines that were removed from the MO but are still in the bom are
|
||||
used in the BoM cost computing. Lastly, it makes sure that Kits are also accounted
|
||||
for and used in the BoM cost as they should.
|
||||
"""
|
||||
# Create a color variant, which will be used to create a Product
|
||||
attribute_color = self.env['product.attribute'].create({'name': 'Color'})
|
||||
value_black, value_white = self.env['product.attribute.value'].create([
|
||||
{
|
||||
'name': 'Black',
|
||||
'attribute_id': attribute_color.id,
|
||||
},
|
||||
{
|
||||
'name': 'White',
|
||||
'attribute_id': attribute_color.id,
|
||||
},
|
||||
])
|
||||
# Create 3 product templates, one for the variants, to check to not all variants are used
|
||||
# to compute the cost of the MO, another one to make sure the 'missing components' are still
|
||||
# taken into account (they are the product that were removed from the MO but are still present
|
||||
# in the BoM), and the last one to make sure that kits are still taken in as well.
|
||||
product_variants, missing_product, kit_product = self.env['product.template'].create([
|
||||
{
|
||||
'name': 'Variant Product',
|
||||
'type': 'consu',
|
||||
'attribute_line_ids': [Command.create(
|
||||
{
|
||||
'attribute_id': attribute_color.id,
|
||||
'value_ids': [
|
||||
Command.link(value_black.id),
|
||||
Command.link(value_white.id),
|
||||
],
|
||||
},
|
||||
)],
|
||||
},
|
||||
{
|
||||
'name': 'Missing Component',
|
||||
'type': 'consu',
|
||||
'standard_price': 40,
|
||||
},
|
||||
{
|
||||
'name': 'Kit Product',
|
||||
'type': 'consu',
|
||||
'standard_price': 60,
|
||||
},
|
||||
])
|
||||
|
||||
variant_black = product_variants.product_variant_ids[0]
|
||||
variant_white = product_variants.product_variant_ids[1]
|
||||
variant_black.standard_price = 50
|
||||
variant_white.standard_price = 30
|
||||
bom_normal, _ = self.env['mrp.bom'].create([
|
||||
{
|
||||
'product_tmpl_id': product_variants.id,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [Command.create({
|
||||
'product_id': missing_product.product_variant_id.id,
|
||||
'product_qty': 1,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': kit_product.product_variant_id.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
},
|
||||
{
|
||||
'product_tmpl_id': kit_product.id,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [Command.create({
|
||||
'product_id': missing_product.product_variant_id.id,
|
||||
'product_qty': 1,
|
||||
})]
|
||||
},
|
||||
])
|
||||
white_tmpl = bom_normal.product_tmpl_id.product_variant_ids.product_template_attribute_value_ids.filtered(lambda tmpl: tmpl.product_attribute_value_id == value_white)
|
||||
black_tmpl = bom_normal.product_tmpl_id.product_variant_ids.product_template_attribute_value_ids.filtered(lambda tmpl: tmpl.product_attribute_value_id == value_black)
|
||||
black_white_product, black_product, white_product = self.env['product.product'].create([{
|
||||
'name': 'Black White',
|
||||
'type': 'consu',
|
||||
'standard_price': 25,
|
||||
},
|
||||
{
|
||||
'name': 'Black Only',
|
||||
'type': 'consu',
|
||||
'standard_price': 15,
|
||||
},
|
||||
{
|
||||
'name': 'White Only',
|
||||
'type': 'consu',
|
||||
'standard_price': 10,
|
||||
},
|
||||
])
|
||||
self.env['mrp.bom.line'].create([{
|
||||
'bom_id': bom_normal.id,
|
||||
'product_id': black_white_product.id,
|
||||
'product_qty': 1,
|
||||
'bom_product_template_attribute_value_ids': [Command.set([black_tmpl.id, white_tmpl.id])],
|
||||
},
|
||||
{
|
||||
'bom_id': bom_normal.id,
|
||||
'product_id': black_product.id,
|
||||
'product_qty': 1,
|
||||
'bom_product_template_attribute_value_ids': [Command.set([black_tmpl.id])],
|
||||
},
|
||||
{
|
||||
'bom_id': bom_normal.id,
|
||||
'product_id': white_product.id,
|
||||
'product_qty': 1,
|
||||
'bom_product_template_attribute_value_ids': [Command.set([white_tmpl.id])],
|
||||
}
|
||||
])
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': variant_black.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': bom_normal.id,
|
||||
})
|
||||
mo_report = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(mo_report['data']['extras']['unit_bom_cost'], mo_report['data']['extras']['unit_mo_cost'], 'The BoM unit cost should be equal to the sum of the products of said BoM')
|
||||
# Check that the missing components (the components that were removed from the MO but are still in the BoM)
|
||||
# are taken into account when computing the BoM cost
|
||||
mo.move_raw_ids.filtered(lambda m: m.product_id == missing_product.product_variant_id and m.bom_line_id.bom_id.type != 'phantom').unlink()
|
||||
# When a product has two possible variants, and then is deleted, it should be taken in the missing components
|
||||
mo.move_raw_ids.filtered(lambda m: m.product_id == black_white_product and m.bom_line_id.bom_id.type != 'phantom').unlink()
|
||||
mo_report = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(mo_report['data']['extras']['unit_bom_cost'], mo_report['data']['extras']['unit_mo_cost'] + missing_product.standard_price + black_white_product.standard_price, 'The BoM unit cost should take the missing components into account, which are the deleted MO lines')
|
||||
|
||||
def test_mo_overview_operation_cost(self):
|
||||
""" Test that operations correctly compute their cost depending on their cost_mode. """
|
||||
expedition_33 = self.product
|
||||
lumiere = self.env['mrp.workcenter'].create({
|
||||
'name': 'Lumière',
|
||||
'costs_hour': 33,
|
||||
})
|
||||
bom_baguette = self.env['mrp.bom'].create({
|
||||
'product_id': expedition_33.id,
|
||||
'product_tmpl_id': expedition_33.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'operation_ids': [
|
||||
Command.create({
|
||||
'name': 'Get on a boat',
|
||||
'workcenter_id': lumiere.id,
|
||||
'time_cycle': 60,
|
||||
'sequence': 1,
|
||||
'cost_mode': 'actual'
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Die on a beach',
|
||||
'workcenter_id': lumiere.id,
|
||||
'time_cycle': 60,
|
||||
'sequence': 2,
|
||||
'cost_mode': 'estimated'
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
mo = self.env['mrp.production'].create({
|
||||
'name': 'MO',
|
||||
'product_qty': 1.0,
|
||||
'product_id': expedition_33.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
mo.workorder_ids.duration = 10
|
||||
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(overview_values['data']['operations']['details'][0]['mo_cost'], 33.0)
|
||||
self.assertEqual(overview_values['data']['operations']['details'][0]['real_cost'], 5.5)
|
||||
self.assertEqual(overview_values['data']['operations']['details'][1]['mo_cost'], 33.0)
|
||||
self.assertEqual(overview_values['data']['operations']['details'][1]['real_cost'], 33.0)
|
||||
|
||||
# Costs should stay the same for a done MO and/or if the cost_mode of the operation is changed
|
||||
mo.button_mark_done()
|
||||
bom_baguette.operation_ids.filtered(lambda o: o.cost_mode == 'estimated').cost_mode = 'actual'
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(overview_values['data']['operations']['details'][0]['mo_cost'], 33.0)
|
||||
self.assertEqual(overview_values['data']['operations']['details'][0]['real_cost'], 5.5)
|
||||
self.assertEqual(overview_values['data']['operations']['details'][1]['mo_cost'], 33.0)
|
||||
self.assertEqual(overview_values['data']['operations']['details'][1]['real_cost'], 33.0)
|
||||
|
||||
def test_mo_overview_with_different_uom(self):
|
||||
"""Ensure that the MO overview correctly computes costs
|
||||
when the product UoM differs from the BoM UoM.
|
||||
|
||||
In this case, the product is defined in Unit while the BoM
|
||||
is defined in Dozen.
|
||||
"""
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': self.product.id,
|
||||
'product_tmpl_id': self.product.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_dozen').id,
|
||||
'product_qty': 1.0,
|
||||
'bom_line_ids': [Command.create({
|
||||
'product_id': self.product1.id,
|
||||
'product_qty': 12.0,
|
||||
})],
|
||||
})
|
||||
self.product1.standard_price = 10
|
||||
# create MO for 1 dozen of the product
|
||||
mo = self.env['mrp.production'].create({
|
||||
'name': 'MO',
|
||||
'bom_id': self.product.bom_ids.id
|
||||
})
|
||||
|
||||
mo.action_confirm()
|
||||
# check that the mo and bom cost are correctly calculated after mo confirmation
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(overview_values['data']['components'][0]['summary']['bom_cost'], 120)
|
||||
self.assertEqual(overview_values['data']['components'][0]['summary']['mo_cost'], 120)
|
||||
|
||||
# Test without BoM
|
||||
mo_no_bom = self.env['mrp.production'].create({
|
||||
'name': 'MO without BoM',
|
||||
'product_id': self.product.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_dozen').id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': False,
|
||||
'move_raw_ids': [Command.create({
|
||||
'product_id': self.product1.id,
|
||||
'product_uom_qty': 12.0,
|
||||
})],
|
||||
})
|
||||
mo_no_bom.action_confirm()
|
||||
overview_values_no_bom = self.env['report.mrp.report_mo_overview'].get_report_values(mo_no_bom.id)
|
||||
self.assertEqual(overview_values_no_bom['data']['components'][0]['summary']['bom_cost'], 120)
|
||||
self.assertEqual(overview_values_no_bom['data']['components'][0]['summary']['mo_cost'], 120)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from freezegun import freeze_time
|
||||
from datetime import datetime
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -22,9 +24,8 @@ class TestTraceability(TestMrpCommon):
|
|||
def _create_product(self, tracking):
|
||||
return self.env['product.product'].create({
|
||||
'name': 'Product %s' % tracking,
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': tracking,
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
def test_tracking_types_on_mo(self):
|
||||
|
|
@ -34,37 +35,36 @@ class TestTraceability(TestMrpCommon):
|
|||
consumed_no_track = self._create_product('none')
|
||||
consumed_lot = self._create_product('lot')
|
||||
consumed_serial = self._create_product('serial')
|
||||
stock_id = self.env.ref('stock.stock_location_stock').id
|
||||
Lot = self.env['stock.lot']
|
||||
# create inventory
|
||||
quants = self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'location_id': self.stock_location.id,
|
||||
'product_id': consumed_no_track.id,
|
||||
'inventory_quantity': 3
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'location_id': self.stock_location.id,
|
||||
'product_id': consumed_lot.id,
|
||||
'inventory_quantity': 3,
|
||||
'lot_id': Lot.create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id
|
||||
'lot_id': Lot.create({'name': 'L1', 'product_id': consumed_lot.id}).id
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'location_id': self.stock_location.id,
|
||||
'product_id': consumed_serial.id,
|
||||
'inventory_quantity': 1,
|
||||
'lot_id': Lot.create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
|
||||
'lot_id': Lot.create({'name': 'S1', 'product_id': consumed_serial.id}).id
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'location_id': self.stock_location.id,
|
||||
'product_id': consumed_serial.id,
|
||||
'inventory_quantity': 1,
|
||||
'lot_id': Lot.create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
|
||||
'lot_id': Lot.create({'name': 'S2', 'product_id': consumed_serial.id}).id
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'location_id': stock_id,
|
||||
'location_id': self.stock_location.id,
|
||||
'product_id': consumed_serial.id,
|
||||
'inventory_quantity': 1,
|
||||
'lot_id': Lot.create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
|
||||
'lot_id': Lot.create({'name': 'S3', 'product_id': consumed_serial.id}).id
|
||||
})
|
||||
quants.action_apply_inventory()
|
||||
|
||||
|
|
@ -72,20 +72,20 @@ class TestTraceability(TestMrpCommon):
|
|||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': finished_product.id,
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': consumed_no_track.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': consumed_lot.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': consumed_serial.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': consumed_no_track.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': consumed_lot.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': consumed_serial.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = finished_product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_uom_id = self.env.ref('uom.product_uom_unit')
|
||||
mo_form.product_uom_id = self.uom_unit
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
|
@ -93,19 +93,19 @@ class TestTraceability(TestMrpCommon):
|
|||
|
||||
# Start MO production
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
if finished_product.tracking != 'none':
|
||||
mo_form.lot_producing_id = self.env['stock.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id})
|
||||
mo_form.lot_producing_ids.set(self.env['stock.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id}))
|
||||
mo = mo_form.save()
|
||||
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.quantity = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[2], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.quantity = 1
|
||||
details_operation_form.save()
|
||||
mo.move_raw_ids.picked = True
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
|
@ -143,27 +143,27 @@ class TestTraceability(TestMrpCommon):
|
|||
def test_tracking_on_byproducts(self):
|
||||
product_final = self.env['product.product'].create({
|
||||
'name': 'Finished Product',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'Raw 1',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
product_2 = self.env['product.product'].create({
|
||||
'name': 'Raw 2',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
byproduct_1 = self.env['product.product'].create({
|
||||
'name': 'Byproduct 1',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
byproduct_2 = self.env['product.product'].create({
|
||||
'name': 'Byproduct 2',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
bom_1 = self.env['mrp.bom'].create({
|
||||
|
|
@ -174,12 +174,12 @@ class TestTraceability(TestMrpCommon):
|
|||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_1.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': product_2.id, 'product_qty': 1})
|
||||
Command.create({'product_id': product_1.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': product_2.id, 'product_qty': 1}),
|
||||
],
|
||||
'byproduct_ids': [
|
||||
(0, 0, {'product_id': byproduct_1.id, 'product_qty': 1, 'product_uom_id': byproduct_1.uom_id.id}),
|
||||
(0, 0, {'product_id': byproduct_2.id, 'product_qty': 1, 'product_uom_id': byproduct_2.uom_id.id})
|
||||
Command.create({'product_id': byproduct_1.id, 'product_qty': 1, 'product_uom_id': byproduct_1.uom_id.id}),
|
||||
Command.create({'product_id': byproduct_2.id, 'product_qty': 1, 'product_uom_id': byproduct_2.uom_id.id}),
|
||||
]})
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_final
|
||||
|
|
@ -188,124 +188,104 @@ class TestTraceability(TestMrpCommon):
|
|||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.lot_producing_id = self.env['stock.lot'].create({
|
||||
mo.lot_producing_ids = self.env['stock.lot'].create({
|
||||
'product_id': product_final.id,
|
||||
'name': 'Final_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo = mo_form.save()
|
||||
mo.qty_producing = 1
|
||||
mo.set_qty_producing()
|
||||
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': product_1.id,
|
||||
'name': 'Raw_1_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': product_2.id,
|
||||
'name': 'Raw_2_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': byproduct_1.id,
|
||||
'name': 'Byproduct_1_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': byproduct_2.id,
|
||||
'name': 'Byproduct_2_lot_1',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
|
||||
mo.move_raw_ids.picked = True
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
mo_backorder = mo.production_group_id.production_ids[-1]
|
||||
mo_form = Form(mo_backorder)
|
||||
mo_form.lot_producing_id = self.env['stock.lot'].create({
|
||||
mo_form.lot_producing_ids.set(self.env['stock.lot'].create({
|
||||
'product_id': product_final.id,
|
||||
'name': 'Final_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo_form.qty_producing = 1
|
||||
}))
|
||||
mo_backorder = mo_form.save()
|
||||
|
||||
details_operation_form = Form(
|
||||
mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_1),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': product_1.id,
|
||||
'name': 'Raw_1_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_2),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': product_2.id,
|
||||
'name': 'Raw_2_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': byproduct_1.id,
|
||||
'name': 'Byproduct_1_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(
|
||||
mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
|
||||
view=self.env.ref('stock.view_stock_move_operations')
|
||||
)
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.env['stock.lot'].create({
|
||||
'product_id': byproduct_2.id,
|
||||
'name': 'Byproduct_2_lot_2',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
|
||||
mo_backorder.move_raw_ids.picked = True
|
||||
mo_backorder.button_mark_done()
|
||||
|
||||
# self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4)
|
||||
# self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6)
|
||||
|
||||
mo = mo | mo_backorder
|
||||
raw_move_lines = mo.move_raw_ids.mapped('move_line_ids')
|
||||
raw_line_raw_1_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_1')
|
||||
|
|
@ -315,19 +295,19 @@ class TestTraceability(TestMrpCommon):
|
|||
|
||||
finished_move_lines = mo.move_finished_ids.mapped('move_line_ids')
|
||||
finished_move_line_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_1')
|
||||
self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.quantity), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
finished_move_line_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_2')
|
||||
raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2')
|
||||
raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2')
|
||||
self.assertEqual(finished_move_line_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
|
||||
|
||||
byproduct_move_line_1_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_1')
|
||||
self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids.filtered(lambda l: l.quantity), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
byproduct_move_line_1_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_2')
|
||||
self.assertEqual(byproduct_move_line_1_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
|
||||
|
||||
byproduct_move_line_2_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_1')
|
||||
self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.quantity), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
|
||||
byproduct_move_line_2_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_2')
|
||||
self.assertEqual(byproduct_move_line_2_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
|
||||
|
||||
|
|
@ -338,25 +318,24 @@ class TestTraceability(TestMrpCommon):
|
|||
Produce a new SN product with same lot
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='serial')
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.env['stock.quant']._update_available_quantity(p1, stock_location, 1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, stock_location, 1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1)
|
||||
mo.action_assign()
|
||||
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo_form.lot_producing_id = lot
|
||||
mo_form.lot_producing_ids.set(lot)
|
||||
mo = mo_form.save()
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
unbuild_form.lot_id = lot
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -366,33 +345,28 @@ class TestTraceability(TestMrpCommon):
|
|||
|
||||
with self.assertLogs(level="WARNING") as log_catcher:
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo_form.lot_producing_id = lot
|
||||
mo_form.lot_producing_ids.set(lot)
|
||||
mo = mo_form.save()
|
||||
_logger.warning('Dummy')
|
||||
self.assertEqual(len(log_catcher.output), 1, "Useless warnings: \n%s" % "\n".join(log_catcher.output[:-1]))
|
||||
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
||||
def test_tracked_and_manufactured_component(self):
|
||||
"""
|
||||
Suppose this structure:
|
||||
""" Suppose this structure:
|
||||
productA --|- 1 x productB --|- 1 x productC
|
||||
with productB tracked by lot
|
||||
Ensure that, when we already have some qty of productB (with different lots),
|
||||
the user can produce several productA and can then produce some productB again
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
|
||||
picking_type.use_auto_consume_components_lots = True
|
||||
|
||||
productA, productB, productC = self.env['product.product'].create([{
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
}, {
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'lot',
|
||||
}, {
|
||||
'name': 'Product C',
|
||||
|
|
@ -402,7 +376,6 @@ class TestTraceability(TestMrpCommon):
|
|||
lot_B01, lot_B02, lot_B03 = self.env['stock.lot'].create([{
|
||||
'name': 'lot %s' % i,
|
||||
'product_id': productB.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for i in [1, 2, 3]])
|
||||
|
||||
self.env['mrp.bom'].create([{
|
||||
|
|
@ -411,11 +384,11 @@ class TestTraceability(TestMrpCommon):
|
|||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})],
|
||||
'bom_line_ids': [Command.create({'product_id': component.id, 'product_qty': 1})],
|
||||
} for finished, component in [(productA, productB), (productB, productC)]])
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(productB, stock_location, 10, lot_id=lot_B01)
|
||||
self.env['stock.quant']._update_available_quantity(productB, stock_location, 5, lot_id=lot_B02)
|
||||
self.env['stock.quant']._update_available_quantity(productB, self.stock_location, 10, lot_id=lot_B01)
|
||||
self.env['stock.quant']._update_available_quantity(productB, self.stock_location, 5, lot_id=lot_B02)
|
||||
|
||||
# Produce 15 x productA
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -423,10 +396,7 @@ class TestTraceability(TestMrpCommon):
|
|||
mo_form.product_qty = 15
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
action = mo.button_mark_done()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
mo.button_mark_done()
|
||||
|
||||
# Produce 15 x productB
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -436,8 +406,9 @@ class TestTraceability(TestMrpCommon):
|
|||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 15
|
||||
mo_form.lot_producing_id = lot_B03
|
||||
mo_form.lot_producing_ids.set(lot_B03)
|
||||
mo = mo_form.save()
|
||||
mo.lot_producing_ids = lot_B03
|
||||
mo.button_mark_done()
|
||||
|
||||
self.assertEqual(lot_B01.product_qty, 0)
|
||||
|
|
@ -453,10 +424,6 @@ class TestTraceability(TestMrpCommon):
|
|||
for EndProduct A, all three lots' delivery_ids are set to
|
||||
Picking A.
|
||||
"""
|
||||
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
customer_location = self.env.ref('stock.stock_location_customers')
|
||||
|
||||
# Create the three lot-tracked products.
|
||||
subcomponentA = self._create_product('lot')
|
||||
componentA = self._create_product('lot')
|
||||
|
|
@ -466,7 +433,6 @@ class TestTraceability(TestMrpCommon):
|
|||
lot_subcomponentA, lot_componentA, lot_endProductA = self.env['stock.lot'].create([{
|
||||
'name': 'lot %s' % product,
|
||||
'product_id': product.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for product in (subcomponentA, componentA, endproductA)])
|
||||
|
||||
# Create two boms, one for Component A and one for EndProduct A
|
||||
|
|
@ -476,10 +442,10 @@ class TestTraceability(TestMrpCommon):
|
|||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})],
|
||||
'bom_line_ids': [Command.create({'product_id': component.id, 'product_qty': 1})],
|
||||
} for finished, component in [(endproductA, componentA), (componentA, subcomponentA)]])
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(subcomponentA, stock_location, 1, lot_id=lot_subcomponentA)
|
||||
self.env['stock.quant']._update_available_quantity(subcomponentA, self.stock_location, 1, lot_id=lot_subcomponentA)
|
||||
|
||||
# Produce 1 component A
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -488,10 +454,9 @@ class TestTraceability(TestMrpCommon):
|
|||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.lot_producing_id = lot_componentA
|
||||
mo_form.lot_producing_ids.set(lot_componentA)
|
||||
mo = mo_form.save()
|
||||
mo.move_raw_ids[0].quantity_done = 1.0
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
|
||||
# Produce 1 endProduct A
|
||||
|
|
@ -501,37 +466,34 @@ class TestTraceability(TestMrpCommon):
|
|||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.lot_producing_id = lot_endProductA
|
||||
mo_form.lot_producing_ids.set(lot_endProductA)
|
||||
mo = mo_form.save()
|
||||
mo.move_raw_ids[0].quantity_done = 1.0
|
||||
mo.move_raw_ids[0].write({'quantity': 1.0, 'picked': True})
|
||||
mo.button_mark_done()
|
||||
|
||||
# Create out picking for EndProduct A
|
||||
pickingA_out = self.env['stock.picking'].create({
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': stock_location.id,
|
||||
'location_dest_id': customer_location.id})
|
||||
'picking_type_id': self.picking_type_out.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
})
|
||||
|
||||
moveA = self.env['stock.move'].create({
|
||||
'name': 'Picking A move',
|
||||
'product_id': endproductA.id,
|
||||
'product_uom_qty': 1,
|
||||
'quantity': 1,
|
||||
'product_uom': endproductA.uom_id.id,
|
||||
'picking_id': pickingA_out.id,
|
||||
'location_id': stock_location.id,
|
||||
'location_dest_id': customer_location.id})
|
||||
|
||||
# Confirm and assign pickingA
|
||||
pickingA_out.action_confirm()
|
||||
pickingA_out.action_assign()
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
})
|
||||
|
||||
# Set move_line lot_id to the mrp.production lot_producing_id
|
||||
moveA.move_line_ids[0].write({
|
||||
'qty_done': 1.0,
|
||||
'quantity': 1.0,
|
||||
'lot_id': lot_endProductA.id,
|
||||
})
|
||||
# Transfer picking
|
||||
moveA.picked = True
|
||||
pickingA_out._action_done()
|
||||
|
||||
# Use concat so that delivery_ids is computed in batch.
|
||||
|
|
@ -544,20 +506,16 @@ class TestTraceability(TestMrpCommon):
|
|||
Build a product P that uses C with SN, unbuild P, scrap SN, unscrap SN
|
||||
and rebuild a product with SN in the components
|
||||
"""
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
stock_location = warehouse.lot_stock_id
|
||||
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
serial_number = self.env['stock.lot'].create({
|
||||
'product_id': component.id,
|
||||
'name': 'Super Serial',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=serial_number)
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=serial_number)
|
||||
|
||||
# produce 1
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -569,18 +527,17 @@ class TestTraceability(TestMrpCommon):
|
|||
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 1
|
||||
mo.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo.move_raw_ids.move_line_ids.quantity = 1
|
||||
mo.button_mark_done()
|
||||
|
||||
# unbuild
|
||||
action = mo.button_unbuild()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.action_validate()
|
||||
Form.from_action(self.env, mo.button_unbuild()).save().action_validate()
|
||||
|
||||
# scrap the component
|
||||
scrap = self.env['stock.scrap'].create({
|
||||
'product_id': component.id,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'scrap_qty': 1,
|
||||
'lot_id': serial_number.id,
|
||||
})
|
||||
|
|
@ -589,18 +546,18 @@ class TestTraceability(TestMrpCommon):
|
|||
|
||||
# unscrap the component
|
||||
internal_move = self.env['stock.move'].create({
|
||||
'name': component.name,
|
||||
'location_id': scrap_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'product_id': component.id,
|
||||
'product_uom': component.uom_id.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'move_line_ids': [(0, 0, {
|
||||
'picked': True,
|
||||
'move_line_ids': [Command.create({
|
||||
'product_id': component.id,
|
||||
'location_id': scrap_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
'qty_done': 1.0,
|
||||
'quantity': 1.0,
|
||||
'lot_id': serial_number.id,
|
||||
})],
|
||||
})
|
||||
|
|
@ -617,12 +574,13 @@ class TestTraceability(TestMrpCommon):
|
|||
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 1
|
||||
mo.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo.move_raw_ids.move_line_ids.quantity = 1
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
|
||||
self.assertRecordValues((mo.move_finished_ids + mo.move_raw_ids).move_line_ids, [
|
||||
{'product_id': self.bom_4.product_id.id, 'lot_id': False, 'qty_done': 1},
|
||||
{'product_id': component.id, 'lot_id': serial_number.id, 'qty_done': 1},
|
||||
{'product_id': self.bom_4.product_id.id, 'lot_id': False, 'quantity': 1},
|
||||
{'product_id': component.id, 'lot_id': serial_number.id, 'quantity': 1},
|
||||
])
|
||||
|
||||
def test_generate_serial_button(self):
|
||||
|
|
@ -633,30 +591,150 @@ class TestTraceability(TestMrpCommon):
|
|||
|
||||
# generate lot lot_0 on MO
|
||||
mo.action_generate_serial()
|
||||
lot_0 = mo.lot_producing_id.name
|
||||
lot_0 = mo.lot_producing_ids[:1].name
|
||||
# manually create lot_1 (lot_0 + 1)
|
||||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': str(int(lot_0) + 1).zfill(7),
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
}).name
|
||||
# generate lot lot_2 on a new MO
|
||||
mo = mo.copy()
|
||||
mo.action_confirm()
|
||||
mo.action_generate_serial()
|
||||
lot_2 = mo.lot_producing_id.name
|
||||
lot_2 = mo.lot_producing_ids.name
|
||||
self.assertEqual(lot_2, str(int(lot_1) + 1).zfill(7))
|
||||
|
||||
def test_generate_serial_button_sequence(self):
|
||||
"""Test if serial in form "00000dd" is manually created, the generate serial
|
||||
correctly create new serial from sequence.
|
||||
"""
|
||||
mo, _bom, p_final, _p1, _p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='serial')
|
||||
p_final.serial_prefix_format = 'TEST/'
|
||||
|
||||
# manually create lot_1
|
||||
self.env['stock.lot'].create({
|
||||
'name': 'TEST/0000001',
|
||||
'product_id': p_final.id,
|
||||
})
|
||||
|
||||
# generate serial lot_2 from the MO (next_serial)
|
||||
mo.action_generate_serial()
|
||||
self.assertEqual(mo.lot_producing_ids[:1].name, "TEST/0000002")
|
||||
|
||||
p_final.lot_sequence_id.prefix = 'xx%(doy)sxx'
|
||||
# generate serial lot_3 from the MO (next from sequence)
|
||||
mo.lot_producing_ids = self.env['stock.lot']
|
||||
mo.action_generate_serial()
|
||||
self.assertIn(datetime.now().strftime('%j'), mo.lot_producing_ids.name)
|
||||
|
||||
def test_use_customized_serial_sequence(self):
|
||||
"""
|
||||
Test that serial numbers are generated with the correct prefix and sequence,
|
||||
that manually provided serial numbers are correctly applied, and that
|
||||
serial numbering remains consistent across multiple manufacturing orders.
|
||||
"""
|
||||
mo, bom, final_product, _comp_1, _comp_2 = self.generate_mo(
|
||||
tracking_final='serial',
|
||||
qty_base_1=1,
|
||||
qty_base_2=1,
|
||||
qty_final=2,
|
||||
)
|
||||
final_product.lot_sequence_id.prefix = 'TEST'
|
||||
final_product.lot_sequence_id.number_next_actual = 1
|
||||
serials_wizard = Form.from_action(self.env, mo.action_generate_serial())
|
||||
self.assertEqual(serials_wizard.lot_name, 'TEST0000001')
|
||||
serials_wizard.save().action_generate_serial_numbers()
|
||||
serials_wizard.save().action_apply()
|
||||
self.assertRecordValues(mo.lot_producing_ids.sorted('name'), [
|
||||
{'name': 'TEST0000001'},
|
||||
{'name': 'TEST0000002'},
|
||||
])
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(final_product, self.stock_location), 2)
|
||||
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', final_product.id)]).sorted('name'), [
|
||||
{'name': 'TEST0000001'},
|
||||
{'name': 'TEST0000002'},
|
||||
])
|
||||
self.assertEqual(final_product.serial_prefix_format + final_product.next_serial, 'TEST0000003')
|
||||
|
||||
second_mo = self.env['mrp.production'].create({
|
||||
'product_id': final_product.id,
|
||||
'bom_id': bom.id,
|
||||
'product_qty': 5,
|
||||
})
|
||||
second_mo.action_confirm()
|
||||
second_serials_wizard = Form.from_action(self.env, second_mo.action_generate_serial())
|
||||
self.assertEqual(second_serials_wizard.lot_name, 'TEST0000003')
|
||||
second_serials_wizard.serial_numbers = 'TEST0000005\nLOREM002\nTEST0000003\nIPSUM101\nTEST0000004'
|
||||
second_serials_wizard.save().action_apply()
|
||||
self.assertRecordValues(second_mo.lot_producing_ids.sorted('name'), [
|
||||
{'name': 'IPSUM101'},
|
||||
{'name': 'LOREM002'},
|
||||
{'name': 'TEST0000003'},
|
||||
{'name': 'TEST0000004'},
|
||||
{'name': 'TEST0000005'},
|
||||
])
|
||||
second_mo.button_mark_done()
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(final_product, self.stock_location), 2 + 5)
|
||||
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', final_product.id)]).sorted('name'), [
|
||||
{'name': 'IPSUM101'},
|
||||
{'name': 'LOREM002'},
|
||||
{'name': 'TEST0000001'},
|
||||
{'name': 'TEST0000002'},
|
||||
{'name': 'TEST0000003'},
|
||||
{'name': 'TEST0000004'},
|
||||
{'name': 'TEST0000005'},
|
||||
])
|
||||
|
||||
third_mo = self.env['mrp.production'].create({
|
||||
'product_id': final_product.id,
|
||||
'bom_id': bom.id,
|
||||
'product_qty': 2,
|
||||
})
|
||||
third_mo.action_confirm()
|
||||
third_serials_wizard = Form.from_action(self.env, third_mo.action_generate_serial())
|
||||
self.assertEqual(third_serials_wizard.lot_name, 'TEST0000006')
|
||||
|
||||
@freeze_time('2024-02-03')
|
||||
def test_interpolation_in_batch_serials(self):
|
||||
"""
|
||||
Test that prefixes are correctly interpolated when
|
||||
generating multiple serial numbers in one MO
|
||||
"""
|
||||
mo, _bom, final_product, _comp_1, _comp_2 = self.generate_mo(
|
||||
tracking_final='serial',
|
||||
qty_base_1=1,
|
||||
qty_base_2=1,
|
||||
qty_final=2,
|
||||
)
|
||||
final_product.lot_sequence_id.prefix = '%(day)s-%(month)s-'
|
||||
final_product.lot_sequence_id.number_next_actual = 1
|
||||
serials_wizard = Form.from_action(self.env, mo.action_generate_serial())
|
||||
self.assertEqual(serials_wizard.lot_name, '03-02-0000001')
|
||||
serials_wizard.save().action_generate_serial_numbers()
|
||||
serials_wizard.save().action_apply()
|
||||
self.assertRecordValues(mo.lot_producing_ids.sorted('name'), [
|
||||
{'name': '03-02-0000001'},
|
||||
{'name': '03-02-0000002'},
|
||||
])
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(final_product, self.stock_location), 2)
|
||||
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', final_product.id)]).sorted('name'), [
|
||||
{'name': '03-02-0000001'},
|
||||
{'name': '03-02-0000002'},
|
||||
])
|
||||
self.assertEqual(final_product.next_serial, '0000003')
|
||||
|
||||
def test_assign_stock_move_date_on_mark_done(self):
|
||||
product_final = self.env['product.product'].create({
|
||||
'name': 'Finished Product',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
with freeze_time('2024-01-15'):
|
||||
production = self.env['mrp.production'].create({
|
||||
'product_id': product_final.id,
|
||||
'product_qty': 1,
|
||||
'date_planned_start': datetime(2024, 1, 10)
|
||||
'date_start': datetime(2024, 1, 10)
|
||||
})
|
||||
production.action_confirm()
|
||||
production.qty_producing = 1
|
||||
|
|
@ -670,10 +748,9 @@ class TestTraceability(TestMrpCommon):
|
|||
Consume SN
|
||||
Consume SN -> Should raise an error as it has already been consumed
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
|
|
@ -682,7 +759,7 @@ class TestTraceability(TestMrpCommon):
|
|||
'name': name,
|
||||
'company_id': self.env.company.id,
|
||||
} for name in ['SN01', 'SN02']])
|
||||
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=sn_lot02)
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=sn_lot02)
|
||||
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': component.id,
|
||||
|
|
@ -691,11 +768,10 @@ class TestTraceability(TestMrpCommon):
|
|||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1
|
||||
mo.lot_producing_id = sn_lot01
|
||||
mo.lot_producing_ids = sn_lot01
|
||||
mo.button_mark_done()
|
||||
self.assertRecordValues(mo.move_finished_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn_lot01.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
{'product_id': component.id, 'lot_id': sn_lot01.id, 'quantity': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -703,11 +779,12 @@ class TestTraceability(TestMrpCommon):
|
|||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1
|
||||
mo.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo.move_raw_ids.move_line_ids.quantity = 1
|
||||
mo.move_raw_ids.move_line_ids.lot_id = sn_lot01
|
||||
mo.move_raw_ids.move_line_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
self.assertRecordValues(mo.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn_lot01.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
{'product_id': component.id, 'lot_id': sn_lot01.id, 'quantity': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -715,8 +792,9 @@ class TestTraceability(TestMrpCommon):
|
|||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1
|
||||
mo.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo.move_raw_ids.move_line_ids.quantity = 1
|
||||
mo.move_raw_ids.move_line_ids.lot_id = sn_lot01
|
||||
mo.move_raw_ids.move_line_ids.picked = True
|
||||
with self.assertRaises(UserError):
|
||||
mo.button_mark_done()
|
||||
|
||||
|
|
@ -730,7 +808,7 @@ class TestTraceability(TestMrpCommon):
|
|||
"""
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
|
|
@ -747,8 +825,7 @@ class TestTraceability(TestMrpCommon):
|
|||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo_produce_sn.action_confirm()
|
||||
mo_produce_sn.qty_producing = 1
|
||||
mo_produce_sn.lot_producing_id = sn
|
||||
mo_produce_sn.lot_producing_ids = sn
|
||||
mo_produce_sn.button_mark_done()
|
||||
|
||||
mo_consume_sn_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -756,11 +833,12 @@ class TestTraceability(TestMrpCommon):
|
|||
mo_consume_sn = mo_consume_sn_form.save()
|
||||
mo_consume_sn.action_confirm()
|
||||
mo_consume_sn.qty_producing = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.quantity = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.picked = True
|
||||
mo_consume_sn.button_mark_done()
|
||||
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'quantity': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
|
|
@ -772,11 +850,12 @@ class TestTraceability(TestMrpCommon):
|
|||
mo_consume_sn = mo_consume_sn_form.save()
|
||||
mo_consume_sn.action_confirm()
|
||||
mo_consume_sn.qty_producing = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.quantity = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.picked = True
|
||||
mo_consume_sn.button_mark_done()
|
||||
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'quantity': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
def test_produce_consume_unbuild_all_and_consume(self):
|
||||
|
|
@ -789,10 +868,9 @@ class TestTraceability(TestMrpCommon):
|
|||
Consume SN
|
||||
-> We should not raise any UserError
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
component.write({
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
|
|
@ -809,8 +887,7 @@ class TestTraceability(TestMrpCommon):
|
|||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo_produce_sn.action_confirm()
|
||||
mo_produce_sn.qty_producing = 1
|
||||
mo_produce_sn.lot_producing_id = sn
|
||||
mo_produce_sn.lot_producing_ids = sn
|
||||
mo_produce_sn.button_mark_done()
|
||||
|
||||
mo_consume_sn_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -818,11 +895,12 @@ class TestTraceability(TestMrpCommon):
|
|||
mo_consume_sn = mo_consume_sn_form.save()
|
||||
mo_consume_sn.action_confirm()
|
||||
mo_consume_sn.qty_producing = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.quantity = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.picked = True
|
||||
mo_consume_sn.button_mark_done()
|
||||
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'quantity': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
|
|
@ -831,18 +909,20 @@ class TestTraceability(TestMrpCommon):
|
|||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo_produce_sn
|
||||
unbuild_form.lot_id = sn
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=sn)
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=sn)
|
||||
|
||||
mo_consume_sn_form = Form(self.env['mrp.production'])
|
||||
mo_consume_sn_form.bom_id = self.bom_4
|
||||
mo_consume_sn = mo_consume_sn_form.save()
|
||||
mo_consume_sn.action_confirm()
|
||||
mo_consume_sn.qty_producing = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.qty_done = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.quantity = 1
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.lot_id = sn
|
||||
mo_consume_sn.move_raw_ids.move_line_ids.picked = True
|
||||
mo_consume_sn.button_mark_done()
|
||||
self.assertRecordValues(mo_consume_sn.move_raw_ids.move_line_ids, [
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'qty_done': 1.0, 'state': 'done'},
|
||||
{'product_id': component.id, 'lot_id': sn.id, 'quantity': 1.0, 'state': 'done'},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.exceptions import UserError
|
||||
|
|
@ -10,9 +11,8 @@ class TestUnbuild(TestMrpCommon):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
cls.env.ref('base.group_user').write({
|
||||
'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]
|
||||
'implied_ids': [Command.link(cls.env.ref('stock.group_production_lot').id)],
|
||||
})
|
||||
|
||||
def test_unbuild_standart(self):
|
||||
|
|
@ -87,7 +87,6 @@ class TestUnbuild(TestMrpCommon):
|
|||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
|
|
@ -96,7 +95,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo_form.lot_producing_id = lot
|
||||
mo_form.lot_producing_ids.set(lot)
|
||||
mo = mo_form.save()
|
||||
|
||||
mo.button_mark_done()
|
||||
|
|
@ -152,7 +151,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
def test_unbuild_with_comnsumed_lot(self):
|
||||
def test_unbuild_with_consumed_lot(self):
|
||||
""" This test creates a MO and then creates 3 unbuild
|
||||
orders for the final product. Only once of the two consumed
|
||||
product is tracked by lot. It checks the stock state after each
|
||||
|
|
@ -164,7 +163,6 @@ class TestUnbuild(TestMrpCommon):
|
|||
lot = self.env['stock.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot)
|
||||
|
|
@ -181,9 +179,10 @@ class TestUnbuild(TestMrpCommon):
|
|||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = lot
|
||||
ml.qty_done = 20
|
||||
ml.quantity = 20
|
||||
details_operation_form.save()
|
||||
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# Check quantity in stock before unbuild.
|
||||
|
|
@ -198,7 +197,6 @@ class TestUnbuild(TestMrpCommon):
|
|||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.product_qty = 3
|
||||
unbuild_order = x.save()
|
||||
|
||||
# This should fail since we do not provide the MO that we wanted to unbuild. (without MO we do not know which consumed lot we have to restore)
|
||||
|
|
@ -208,6 +206,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have consumed 3 final product in stock')
|
||||
|
||||
unbuild_order.mo_id = mo.id
|
||||
unbuild_order.product_qty = 3
|
||||
unbuild_order.action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
|
||||
|
|
@ -248,17 +247,14 @@ class TestUnbuild(TestMrpCommon):
|
|||
lot_final = self.env['stock.lot'].create({
|
||||
'name': 'lot_final',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_consumed_1',
|
||||
'product_id': p1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_consumed_2',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot_1)
|
||||
|
|
@ -268,17 +264,18 @@ class TestUnbuild(TestMrpCommon):
|
|||
# FIXME sle: behavior change
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo_form.lot_producing_id = lot_final
|
||||
mo_form.lot_producing_ids.set(lot_final)
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 5
|
||||
ml.quantity = 5
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 20
|
||||
ml.quantity = 20
|
||||
details_operation_form.save()
|
||||
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# Check quantity in stock before unbuild.
|
||||
|
|
@ -311,6 +308,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.lot_id = lot_final
|
||||
x.product_qty = 3
|
||||
x.save().action_unbuild()
|
||||
|
||||
|
|
@ -322,6 +320,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.lot_id = lot_final
|
||||
x.product_qty = 2
|
||||
x.save().action_unbuild()
|
||||
|
||||
|
|
@ -333,6 +332,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
x.product_id = p_final
|
||||
x.bom_id = bom
|
||||
x.mo_id = mo
|
||||
x.lot_id = lot_final
|
||||
x.product_qty = 5
|
||||
x.save().action_unbuild()
|
||||
|
||||
|
|
@ -350,17 +350,14 @@ class TestUnbuild(TestMrpCommon):
|
|||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_1',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_2',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_3 = self.env['stock.lot'].create({
|
||||
'name': 'lot_3',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=lot_1)
|
||||
|
|
@ -371,15 +368,8 @@ class TestUnbuild(TestMrpCommon):
|
|||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids.filtered(lambda ml: ml.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = ml.reserved_uom_qty
|
||||
with details_operation_form.move_line_ids.edit(1) as ml:
|
||||
ml.qty_done = ml.reserved_uom_qty
|
||||
with details_operation_form.move_line_ids.edit(2) as ml:
|
||||
ml.qty_done = ml.reserved_uom_qty
|
||||
details_operation_form.save()
|
||||
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# Check quantity in stock before unbuild.
|
||||
|
|
@ -412,64 +402,100 @@ class TestUnbuild(TestMrpCommon):
|
|||
lot_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_1',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_1)
|
||||
lot_finished_1 = self.env['stock.lot'].create({
|
||||
'name': 'lot_finished_1',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.assertEqual(mo.product_qty, 5)
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 3.0
|
||||
mo_form.lot_producing_id = lot_finished_1
|
||||
mo_form.lot_producing_ids.set(lot_finished_1)
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(mo.move_raw_ids[1].quantity_done, 12)
|
||||
self.assertEqual(mo.move_raw_ids[1].quantity, 12)
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 3
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.quantity = 3
|
||||
ml.lot_id = lot_1
|
||||
details_operation_form.save()
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env[action['res_model']].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo.move_raw_ids.picked = True
|
||||
Form.from_action(self.env, mo.button_mark_done()).save().action_backorder()
|
||||
|
||||
lot_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_2',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 4, lot_id=lot_2)
|
||||
lot_finished_2 = self.env['stock.lot'].create({
|
||||
'name': 'lot_finished_2',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
mo = mo.procurement_group_id.mrp_production_ids[1]
|
||||
mo = mo.production_group_id.production_ids[1]
|
||||
# FIXME sle: issue in backorder?
|
||||
mo.move_raw_ids.move_line_ids.unlink()
|
||||
self.assertEqual(mo.product_qty, 2)
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 2
|
||||
mo_form.lot_producing_id = lot_finished_2
|
||||
mo_form.lot_producing_ids.set(lot_finished_2)
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 2
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.quantity = 2
|
||||
ml.lot_id = lot_2
|
||||
details_operation_form.save()
|
||||
action = mo.button_mark_done()
|
||||
|
||||
mo1 = mo.procurement_group_id.mrp_production_ids[0]
|
||||
mo1 = mo.production_group_id.production_ids[0]
|
||||
ml = mo1.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_1 in m.produce_line_ids.lot_id)
|
||||
self.assertEqual(sum(ml.mapped('qty_done')), 12.0, 'Should have consumed 12 for the first lot')
|
||||
self.assertEqual(sum(ml.mapped('quantity')), 12.0, 'Should have consumed 12 for the first lot')
|
||||
ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_2 in m.produce_line_ids.lot_id)
|
||||
self.assertEqual(sum(ml.mapped('qty_done')), 8.0, 'Should have consumed 8 for the second lot')
|
||||
self.assertEqual(sum(ml.mapped('quantity')), 8.0, 'Should have consumed 8 for the second lot')
|
||||
|
||||
def test_unbuild_without_lot_after_tracking_change(self):
|
||||
""" This test creates a MO without lots, and later one of the consumed products starts being tracking by lot.
|
||||
And then creates 1 unbuild order for the final product.
|
||||
It checks the stock state after each order and ensure it is correct.
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo()
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
# Check quantity in stock before unbuild.
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
p1.tracking = 'lot'
|
||||
|
||||
# ---------------------------------------------------
|
||||
# unbuild
|
||||
# ---------------------------------------------------
|
||||
|
||||
x = Form(self.env['mrp.unbuild'])
|
||||
x.product_id = p_final
|
||||
x.mo_id = mo
|
||||
x.product_qty = 3
|
||||
unbuild_order = x.save()
|
||||
self.assertEqual(unbuild_order.bom_id, bom, 'Should have filled bom field automatically')
|
||||
unbuild_order.action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, strict=True), 92, 'You should have 92 products in stock without lot')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
|
||||
|
||||
def test_unbuild_with_routes(self):
|
||||
""" This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock
|
||||
|
|
@ -480,22 +506,21 @@ class TestUnbuild(TestMrpCommon):
|
|||
StockQuant = self.env['stock.quant']
|
||||
ProductObj = self.env['product.product']
|
||||
# Create new QC/Unbuild location
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
unbuild_location = self.env['stock.location'].create({
|
||||
'name': 'QC/Unbuild',
|
||||
'usage': 'internal',
|
||||
'location_id': warehouse.view_location_id.id
|
||||
'location_id': self.warehouse_1.view_location_id.id
|
||||
})
|
||||
|
||||
# Create a product route containing a stock rule that will move product from QC/Unbuild location to stock
|
||||
self.env['stock.route'].create({
|
||||
'name': 'QC/Unbuild -> Stock',
|
||||
'warehouse_selectable': True,
|
||||
'warehouse_ids': [(4, warehouse.id)],
|
||||
'rule_ids': [(0, 0, {
|
||||
'warehouse_ids': [Command.link(self.warehouse_1.id)],
|
||||
'rule_ids': [Command.create({
|
||||
'name': 'Send Matrial QC/Unbuild -> Stock',
|
||||
'action': 'push',
|
||||
'picking_type_id': self.ref('stock.picking_type_internal'),
|
||||
'picking_type_id': self.picking_type_int.id,
|
||||
'location_src_id': unbuild_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
})],
|
||||
|
|
@ -504,15 +529,15 @@ class TestUnbuild(TestMrpCommon):
|
|||
# Create a stockable product and its components
|
||||
finshed_product = ProductObj.create({
|
||||
'name': 'Table',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
component1 = ProductObj.create({
|
||||
'name': 'Table head',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
component2 = ProductObj.create({
|
||||
'name': 'Table stand',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
|
||||
# Create bom and add components
|
||||
|
|
@ -523,8 +548,8 @@ class TestUnbuild(TestMrpCommon):
|
|||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component1.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': component2.id, 'product_qty': 1})
|
||||
Command.create({'product_id': component1.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': component2.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
# Set on hand quantity
|
||||
|
|
@ -580,8 +605,8 @@ class TestUnbuild(TestMrpCommon):
|
|||
self.assertEqual(picking.location_dest_id.id, self.stock_location.id, 'Wrong destination location in picking')
|
||||
|
||||
# Transfer it
|
||||
for ml in picking.move_ids_without_package:
|
||||
ml.quantity_done = 1
|
||||
for ml in picking.move_ids:
|
||||
ml.write({'quantity': 1, 'picked': True})
|
||||
picking._action_done()
|
||||
|
||||
# Check the available quantity of components and final product in stock
|
||||
|
|
@ -595,8 +620,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
- decimal accuracy of Product UoM > decimal accuracy of Units
|
||||
- unbuild a product with a decimal quantity of component
|
||||
"""
|
||||
self.env['decimal.precision'].search([('name', '=', 'Product Unit of Measure')]).digits = 4
|
||||
self.uom_unit.rounding = 0.001
|
||||
self.env['decimal.precision'].search([('name', '=', 'Product Unit')]).digits = 4
|
||||
|
||||
self.bom_1.product_qty = 3
|
||||
self.bom_1.bom_line_ids.product_qty = 5
|
||||
|
|
@ -629,17 +653,16 @@ class TestUnbuild(TestMrpCommon):
|
|||
"""
|
||||
compo, finished = self.env['product.product'].create([{
|
||||
'name': 'compo',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
}, {
|
||||
'name': 'finished',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
}])
|
||||
|
||||
lot01, lot02 = self.env['stock.lot'].create([{
|
||||
'name': n,
|
||||
'product_id': compo.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for n in ['lot01', 'lot02']])
|
||||
self.env['stock.quant']._update_available_quantity(compo, self.stock_location, 1, lot_id=lot01)
|
||||
self.env['stock.quant']._update_available_quantity(compo, self.stock_location, 1, lot_id=lot02)
|
||||
|
|
@ -662,12 +685,13 @@ class TestUnbuild(TestMrpCommon):
|
|||
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.quantity = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.quantity = 1
|
||||
details_operation_form.save()
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
|
||||
uo_form = Form(self.env['mrp.unbuild'])
|
||||
|
|
@ -685,7 +709,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
unbuild order are applied on the stock moves
|
||||
"""
|
||||
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
|
||||
self.env.user.write({'groups_id': [(4, grp_multi_loc.id, 0)]})
|
||||
self.env.user.write({'group_ids': [Command.link(grp_multi_loc.id)]})
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||||
prod_location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.env.user.id)])
|
||||
subloc01, subloc02, = self.stock_location.child_ids[:2]
|
||||
|
|
@ -706,13 +730,11 @@ class TestUnbuild(TestMrpCommon):
|
|||
internal_form.picking_type_id = warehouse.int_type_id
|
||||
internal_form.location_id = self.stock_location
|
||||
internal_form.location_dest_id = subloc01
|
||||
with internal_form.move_ids_without_package.new() as move:
|
||||
with internal_form.move_ids.new() as move:
|
||||
move.product_id = p_final
|
||||
move.product_uom_qty = 1.0
|
||||
move.quantity = 1.0
|
||||
move.picked = True
|
||||
internal_transfer = internal_form.save()
|
||||
internal_transfer.action_confirm()
|
||||
internal_transfer.action_assign()
|
||||
internal_transfer.move_line_ids.qty_done = 1.0
|
||||
internal_transfer.button_validate()
|
||||
|
||||
unbuild_order_form = Form(self.env['mrp.unbuild'])
|
||||
|
|
@ -739,9 +761,8 @@ class TestUnbuild(TestMrpCommon):
|
|||
order = self.env['mrp.unbuild'].create({
|
||||
'product_id': self.product_4.id,
|
||||
})
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
self.assertEqual(order.location_id, warehouse.lot_stock_id)
|
||||
self.assertEqual(order.location_dest_id, warehouse.lot_stock_id)
|
||||
self.assertEqual(order.location_id, self.stock_location)
|
||||
self.assertEqual(order.location_dest_id, self.stock_location)
|
||||
|
||||
def test_use_unbuilt_sn_in_mo(self):
|
||||
"""
|
||||
|
|
@ -750,17 +771,16 @@ class TestUnbuild(TestMrpCommon):
|
|||
"""
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'Product tracked by sn',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
product_1_sn = self.env['stock.lot'].create({
|
||||
'name': 'sn1',
|
||||
'product_id': product_1.id,
|
||||
'company_id': self.env.company.id
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Product component',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
bom_1 = self.env['mrp.bom'].create({
|
||||
'product_id': product_1.id,
|
||||
|
|
@ -769,12 +789,12 @@ class TestUnbuild(TestMrpCommon):
|
|||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': component.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
product_2 = self.env['product.product'].create({
|
||||
'name': 'finished Product',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_2.id,
|
||||
|
|
@ -783,7 +803,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_1.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': product_1.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
# mo1
|
||||
|
|
@ -795,8 +815,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
mo.action_confirm()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo_form.lot_producing_id = product_1_sn
|
||||
mo_form.lot_producing_ids.set(product_1_sn)
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
|
@ -804,6 +823,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
#unbuild order
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
unbuild_form.lot_id = product_1_sn
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
#mo2
|
||||
|
|
@ -814,11 +834,12 @@ class TestUnbuild(TestMrpCommon):
|
|||
details_operation_form = Form(mo2.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = product_1_sn
|
||||
ml.qty_done = 1
|
||||
ml.quantity = 1
|
||||
details_operation_form.save()
|
||||
mo_form = Form(mo2)
|
||||
mo_form.qty_producing = 1
|
||||
mo2 = mo_form.save()
|
||||
mo2.move_raw_ids.picked = True
|
||||
mo2.button_mark_done()
|
||||
self.assertEqual(mo2.state, 'done', "Production order should be in done state.")
|
||||
|
||||
|
|
@ -829,26 +850,25 @@ class TestUnbuild(TestMrpCommon):
|
|||
"""
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Product tracked by sn',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'serial',
|
||||
})
|
||||
finished_product_sn = self.env['stock.lot'].create({
|
||||
'name': 'sn1',
|
||||
'product_id': finished_product.id,
|
||||
'company_id': self.env.company.id
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Product component',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
bom_1 = self.env['mrp.bom'].create({
|
||||
'product_id': finished_product.id,
|
||||
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': component.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
# mo_1
|
||||
|
|
@ -858,15 +878,12 @@ class TestUnbuild(TestMrpCommon):
|
|||
mo_form.product_qty = 1.0
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1.0
|
||||
mo.lot_producing_id = finished_product_sn
|
||||
mo.move_raw_ids.quantity_done = 1
|
||||
mo.lot_producing_ids = finished_product_sn
|
||||
mo.move_raw_ids.write({'quantity': 1, 'picked': True})
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
# unbuild order mo_1
|
||||
action = mo.button_unbuild()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.action_validate()
|
||||
Form.from_action(self.env, mo.button_unbuild()).save().action_validate()
|
||||
self.assertEqual(mo.unbuild_ids.produce_line_ids[0].product_id, finished_product)
|
||||
self.assertEqual(mo.unbuild_ids.produce_line_ids[0].lot_ids, finished_product_sn)
|
||||
self.assertEqual(mo.unbuild_ids.produce_line_ids[1].product_id, component)
|
||||
|
|
@ -877,7 +894,6 @@ class TestUnbuild(TestMrpCommon):
|
|||
component_sn = self.env['stock.lot'].create({
|
||||
'name': 'component-sn1',
|
||||
'product_id': component.id,
|
||||
'company_id': self.env.company.id
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=component_sn)
|
||||
#mo2 with tracked component
|
||||
|
|
@ -887,15 +903,13 @@ class TestUnbuild(TestMrpCommon):
|
|||
mo_form.product_qty = 1.0
|
||||
mo_2 = mo_form.save()
|
||||
mo_2.action_confirm()
|
||||
mo_2.qty_producing = 1.0
|
||||
mo_2.lot_producing_id = finished_product_sn
|
||||
mo_2.move_raw_ids.quantity_done = 1
|
||||
mo_2.lot_producing_ids = finished_product_sn
|
||||
mo_2.move_raw_ids.write({'quantity': 1, 'picked': True})
|
||||
mo_2.button_mark_done()
|
||||
self.assertEqual(mo_2.state, 'done', "Production order should be in done state.")
|
||||
# unbuild mo_2
|
||||
action = mo_2.button_unbuild()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.action_validate()
|
||||
Form.from_action(self.env, action).save().action_validate()
|
||||
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[0].product_id, finished_product)
|
||||
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[0].lot_ids, finished_product_sn)
|
||||
self.assertEqual(mo_2.unbuild_ids.produce_line_ids[1].product_id, component)
|
||||
|
|
@ -921,6 +935,7 @@ class TestUnbuild(TestMrpCommon):
|
|||
mo = mo_form.save()
|
||||
|
||||
mo.action_confirm()
|
||||
mo.move_finished_ids._do_unreserve()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 4
|
||||
mo = mo_form.save()
|
||||
|
|
@ -941,42 +956,233 @@ class TestUnbuild(TestMrpCommon):
|
|||
unbuild_order.action_unbuild()
|
||||
self.assertRecordValues(unbuild_order.produce_line_ids.move_line_ids, [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'product_id': self.bom_1.product_id.id, 'qty_done': 3},
|
||||
{'product_id': self.bom_1.bom_line_ids[0].product_id.id, 'qty_done': 0.6},
|
||||
{'product_id': self.bom_1.bom_line_ids[1].product_id.id, 'qty_done': 1.2},
|
||||
{'product_id': self.bom_1.product_id.id, 'quantity': 3},
|
||||
{'product_id': self.bom_1.bom_line_ids[0].product_id.id, 'quantity': 0.6},
|
||||
{'product_id': self.bom_1.bom_line_ids[1].product_id.id, 'quantity': 1.2},
|
||||
])
|
||||
|
||||
def test_unbuild_update_forecasted_qty(self):
|
||||
def test_unbuild_less_quantity_consumed(self):
|
||||
"""
|
||||
Test that the unbuild correctly updates the forecasted quantity of a product.
|
||||
Tests that you don't unbuild more than you consumed during production.
|
||||
BoM uses component x20, but only 15 are consumed during the production order.
|
||||
Unbuilding the MO should only put 15 components back in stock.
|
||||
"""
|
||||
bom = self.bom_4
|
||||
product = bom.product_id
|
||||
# qty_available + incoming_qty - outgoing_qty = virtual_available
|
||||
# Currently: 0.0 + 0.0 - 0.0 = 0.0
|
||||
self.assertRecordValues(product, [{"qty_available": 0.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 0.0}])
|
||||
# Manufacture 20 unit
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': self.product_2.id,
|
||||
'product_tmpl_id': self.product_2.product_tmpl_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
Command.create({'product_id': self.product_3.id, 'product_qty': 20}),
|
||||
]
|
||||
})
|
||||
|
||||
with Form(self.env['mrp.production']) as mo_form:
|
||||
mo_form.product_id = self.product_2
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
mo.qty_producing = 1.0
|
||||
mo.move_raw_ids.write({'quantity': 15, 'picked': True})
|
||||
mo.button_mark_done()
|
||||
|
||||
Form.from_action(self.env, mo.button_unbuild()).save().action_validate()
|
||||
self.assertEqual(mo.unbuild_ids.produce_line_ids.filtered(lambda m: m.product_id == self.product_3).product_uom_qty, 15)
|
||||
|
||||
def test_unbuild_mo_different_qty(self):
|
||||
# Test the unbuild of a MO with qty_produced > product_qty
|
||||
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': self.product_2.id,
|
||||
'product_tmpl_id': self.product_2.product_tmpl_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [Command.create({'product_id': self.product_3.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
with Form(self.env['mrp.production']) as mo_form:
|
||||
mo_form.product_id = self.product_2
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 10
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
|
||||
mo.qty_producing = 12
|
||||
mo.move_raw_ids.write({'quantity': 12, 'picked': True})
|
||||
mo.button_mark_done()
|
||||
|
||||
unbuild_action = mo.button_unbuild()
|
||||
unbuild_action['context']['default_product_qty'] = 12
|
||||
Form.from_action(self.env, unbuild_action).save().action_validate()
|
||||
|
||||
unbuild_fns_move = mo.unbuild_ids.produce_line_ids.filtered(lambda m: m.product_id == self.product_2)
|
||||
self.assertEqual(len(unbuild_fns_move), 1)
|
||||
self.assertEqual(unbuild_fns_move.state, "done")
|
||||
self.assertEqual(unbuild_fns_move.quantity, 12)
|
||||
|
||||
def test_putaway_strategy_with_unbuild(self):
|
||||
"""
|
||||
Test that the putaway strategy is correctly applied when unbuilding a product
|
||||
"""
|
||||
# Create a putaway strategy for the component product
|
||||
putaway_strategy = self.env["stock.putaway.rule"].create({
|
||||
"location_in_id": self.stock_location.id,
|
||||
"location_out_id": self.shelf_1.id,
|
||||
'product_id': self.bom_4.bom_line_ids.product_id.id,
|
||||
})
|
||||
# Create a MO for the finished product
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': self.bom_4.product_id.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': self.bom_4.id,
|
||||
'product_uom_id': self.bom_4.product_uom_id.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
mo.qty_producing = 1.0
|
||||
mo.move_raw_ids.write({'quantity': 1, 'picked': True})
|
||||
mo.button_mark_done()
|
||||
|
||||
# Unbuild the MO and check that the putaway strategy is applied for the component product
|
||||
unbuild_action = mo.button_unbuild()
|
||||
unbuild_action['context']['default_product_qty'] = 1
|
||||
unbuild_order = Form.from_action(self.env, unbuild_action).save()
|
||||
unbuild_order.action_unbuild()
|
||||
|
||||
component_move_unbuild = unbuild_order.produce_line_ids.filtered(lambda m: m.product_id == self.bom_4.bom_line_ids.product_id)
|
||||
self.assertEqual(component_move_unbuild.move_line_ids.location_dest_id, putaway_strategy.location_out_id)
|
||||
|
||||
def test_unbuild_consigned_comp(self):
|
||||
""" Test that after unbuild, consigned quant still have the same owner as before the MO."""
|
||||
consigned_partner = self.env['res.partner'].create({'name': 'consigned partner'})
|
||||
mo, _, _, p1, _ = self.generate_mo(qty_final=1, qty_base_1=7)
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 3)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 4, owner_id=consigned_partner)
|
||||
|
||||
mo.action_assign()
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 7)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, owner_id=consigned_partner), 4)
|
||||
|
||||
def test_unbuild_non_storable_product(self):
|
||||
"""Check that the move values of an unbuild of a non-storable product are correct.
|
||||
"""
|
||||
self.product_4.is_storable = False
|
||||
self.product_3.is_storable = False
|
||||
|
||||
self.env['mrp.bom.byproduct'].create({
|
||||
'bom_id': self.bom_1.id,
|
||||
'product_id': self.product_3.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': self.product_3.uom_id.id
|
||||
})
|
||||
|
||||
# Create mo
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 20.0
|
||||
mo_form.product_id = self.product_4
|
||||
mo_form.bom_id = self.bom_1
|
||||
mo_form.product_uom_id = self.product_4.uom_id
|
||||
mo_form.product_qty = 4.0
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertRecordValues(product, [{"qty_available": 0.0, "incoming_qty": 20.0, "outgoing_qty": 0.0, "virtual_available": 20.0}])
|
||||
mo.qty_producing = 20.0
|
||||
mo.move_raw_ids.quantity_done = 20.0
|
||||
|
||||
# Produce the final product
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 4.0
|
||||
mo_form.save()
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertRecordValues(product, [{"qty_available": 20.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 20.0}])
|
||||
# Unlock the MO and add 10 additional produced quantity
|
||||
mo.action_toggle_is_locked()
|
||||
with Form(mo) as mo_form:
|
||||
mo_form.qty_producing = 30.0
|
||||
mo = mo_form.save()
|
||||
self.assertRecordValues(product, [{"qty_available": 30.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 30.0}])
|
||||
# Unbuild the 15 units
|
||||
action = mo.button_unbuild()
|
||||
unbuild_form = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
unbuild_form.product_qty = 15.0
|
||||
wizard = unbuild_form.save()
|
||||
wizard.action_validate()
|
||||
self.assertRecordValues(product, [{"qty_available": 15.0, "incoming_qty": 0.0, "outgoing_qty": 0.0, "virtual_available": 15.0}])
|
||||
|
||||
unbuild_wizard = Form(self.env['mrp.unbuild'])
|
||||
unbuild_wizard.mo_id = mo
|
||||
unbuild = unbuild_wizard.save()
|
||||
unbuild.action_unbuild()
|
||||
|
||||
self.assertRecordValues(unbuild.produce_line_ids, [
|
||||
{'product_id': self.product_4.id, 'quantity': 4, 'state': 'done'}, # Stick
|
||||
{'product_id': self.product_3.id, 'quantity': 12, 'state': 'done'}, # Stone
|
||||
{'product_id': self.product_2.id, 'quantity': 24, 'state': 'done'}, # Wood
|
||||
{'product_id': self.product_1.id, 'quantity': 48, 'state': 'done'}, # Courage
|
||||
])
|
||||
|
||||
def test_unbuild_tracked_component_multiple_unbuilds_same_mo(self):
|
||||
"""
|
||||
Create a Manufacturing Order producing 2 units of a serial-tracked finished product
|
||||
from a serial-tracked component, and verify that during two successive unbuilds,
|
||||
the correct component serial numbers are restored.
|
||||
|
||||
- The MO produces 2 units of P1, consuming serials SN1 and SN2 of C1
|
||||
- Unbuild the first P1 → serial SN1 of C1 is restored
|
||||
- Unbuild the second P1 → serial SN2 of C1 is restored (SN1 must not be reused)
|
||||
"""
|
||||
(self.bom_4.product_id | self.bom_4.bom_line_ids.product_id).is_storable = True
|
||||
(self.bom_4.product_id | self.bom_4.bom_line_ids.product_id).tracking = 'serial'
|
||||
component = self.bom_4.bom_line_ids.product_id
|
||||
# Serials for component
|
||||
sn1 = self.env['stock.lot'].create({
|
||||
'name': 'SN1',
|
||||
'product_id': component.id,
|
||||
})
|
||||
sn2 = self.env['stock.lot'].create({
|
||||
'name': 'SN2',
|
||||
'product_id': component.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=sn1)
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location, 1, lot_id=sn2)
|
||||
# Serials for finished product
|
||||
fp_sn1 = self.env['stock.lot'].create({
|
||||
'name': 'SN1-P1',
|
||||
'product_id': self.bom_4.product_id.id,
|
||||
})
|
||||
fp_sn2 = self.env['stock.lot'].create({
|
||||
'name': 'SN2-P1',
|
||||
'product_id': self.bom_4.product_id.id,
|
||||
})
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': self.bom_4.product_id.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': self.bom_4.id,
|
||||
'product_uom_id': self.bom_4.product_uom_id.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
mo.lot_producing_ids = fp_sn1 + fp_sn2
|
||||
mo._set_qty_producing()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, sn1 + sn2)
|
||||
|
||||
unbuild_1 = self.env['mrp.unbuild'].create({
|
||||
'mo_id': mo.id,
|
||||
'product_id': self.bom_4.product_id.id,
|
||||
'lot_id': fp_sn1.id,
|
||||
})
|
||||
unbuild_1.action_unbuild()
|
||||
self.assertEqual(unbuild_1.state, 'done')
|
||||
self.assertEqual(
|
||||
self.env['stock.quant']._get_available_quantity(component, self.stock_location, lot_id=sn1),
|
||||
1, 'SN1 should be restored after first unbuild'
|
||||
)
|
||||
unbuild_2 = self.env['mrp.unbuild'].create({
|
||||
'mo_id': mo.id,
|
||||
'product_id': self.bom_4.product_id.id,
|
||||
'lot_id': fp_sn2.id,
|
||||
})
|
||||
unbuild_2.action_unbuild()
|
||||
self.assertEqual(unbuild_2.state, 'done')
|
||||
self.assertEqual(
|
||||
self.env['stock.quant']._get_available_quantity(component, self.stock_location, lot_id=sn2),
|
||||
1, 'SN2 should be restored after second unbuild'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,15 +12,19 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Required for `uom_id` to be visible in the view
|
||||
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
|
||||
cls.env.user.group_ids += cls.env.ref('uom.group_uom')
|
||||
cls.env.user.group_ids += cls.env.ref('product.group_product_variant')
|
||||
# Required for `manufacture_steps` to be visible in the view
|
||||
cls.env.user.groups_id += cls.env.ref('stock.group_adv_location')
|
||||
cls.env.user.group_ids += cls.env.ref('stock.group_adv_location')
|
||||
# Required for `product_id` to be visible in the view
|
||||
cls.env.user.group_ids += cls.env.ref('product.group_product_variant')
|
||||
# Create warehouse
|
||||
cls.customer_location = cls.env['ir.model.data']._xmlid_to_res_id('stock.stock_location_customers')
|
||||
warehouse_form = Form(cls.env['stock.warehouse'])
|
||||
warehouse_form.name = 'Test Warehouse'
|
||||
warehouse_form.code = 'TWH'
|
||||
cls.warehouse = warehouse_form.save()
|
||||
# Enable MTO
|
||||
cls.warehouse.mto_pull_id.route_id.active = True
|
||||
|
||||
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
|
||||
|
||||
|
|
@ -28,19 +32,16 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
product_form = Form(cls.env['product.product'])
|
||||
product_form.name = 'Stick'
|
||||
product_form.uom_id = cls.uom_unit
|
||||
product_form.uom_po_id = cls.uom_unit
|
||||
product_form.detailed_type = 'product'
|
||||
product_form.is_storable = True
|
||||
product_form.route_ids.clear()
|
||||
product_form.route_ids.add(cls.warehouse.manufacture_pull_id.route_id)
|
||||
product_form.route_ids.add(cls.warehouse.mto_pull_id.route_id)
|
||||
cls.finished_product = product_form.save()
|
||||
|
||||
# Create raw product for manufactured product
|
||||
product_form = Form(cls.env['product.product'])
|
||||
product_form.name = 'Raw Stick'
|
||||
product_form.detailed_type = 'product'
|
||||
product_form.is_storable = True
|
||||
product_form.uom_id = cls.uom_unit
|
||||
product_form.uom_po_id = cls.uom_unit
|
||||
cls.raw_product = product_form.save()
|
||||
|
||||
# Create bom for manufactured product
|
||||
|
|
@ -85,12 +86,53 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.lot_stock_id.id)
|
||||
|
||||
def test_02_warehouse_twostep_manufacturing(self):
|
||||
""" Warehouse testing for picking ans store after manufacturing """
|
||||
""" Warehouse testing for picking and store after manufacturing """
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
self._check_location_and_routes()
|
||||
self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 3)
|
||||
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.sam_loc_id.id)
|
||||
self.assertEqual(self.warehouse.manufacture_pull_id.location_dest_id.id, self.warehouse.lot_stock_id.id)
|
||||
|
||||
def test_manufacturing_2_steps_sublocation(self):
|
||||
"""Check having a production order taking stock in a child location of pre prod
|
||||
create correctly a 2 steps manufacturing even with only one mto rule from pre-pro to
|
||||
production. """
|
||||
self.warehouse.manufacture_steps = 'pbm'
|
||||
pre_1, pre_2 = self.env['stock.location'].create([{
|
||||
'name': name,
|
||||
'location_id': self.warehouse.pbm_loc_id.id,
|
||||
'usage': 'internal'
|
||||
} for name in ('Pre 1', 'Pre 2')])
|
||||
|
||||
# create 2 picking type having 2 different pre-prod location
|
||||
pick_1 = self.warehouse.manu_type_id.copy({
|
||||
'sequence_code': 'PRE1',
|
||||
'default_location_src_id': pre_1.id,
|
||||
})
|
||||
pick_2 = self.warehouse.manu_type_id.copy({
|
||||
'sequence_code': 'PRE2',
|
||||
'default_location_src_id': pre_2.id,
|
||||
})
|
||||
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.picking_type_id = pick_1
|
||||
production_form.product_id = self.finished_product
|
||||
production = production_form.save()
|
||||
production.action_confirm()
|
||||
# check that picking is created
|
||||
pick = production.picking_ids
|
||||
self.assertEqual(pick.location_id, self.warehouse.lot_stock_id)
|
||||
self.assertEqual(pick.location_dest_id, pre_1)
|
||||
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.picking_type_id = pick_2
|
||||
production_form.product_id = self.finished_product
|
||||
production = production_form.save()
|
||||
production.action_confirm()
|
||||
# check that picking is created
|
||||
pick = production.picking_ids
|
||||
self.assertEqual(pick.location_id, self.warehouse.lot_stock_id)
|
||||
self.assertEqual(pick.location_dest_id, pre_2)
|
||||
|
||||
def test_manufacturing_3_steps(self):
|
||||
""" Test MO/picking before manufacturing/picking after manufacturing
|
||||
|
|
@ -121,6 +163,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
self.assertEqual(len(move_finished_ids), 1)
|
||||
self.assertEqual(move_finished_ids.product_id, self.finished_product)
|
||||
self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
|
||||
production.button_mark_done()
|
||||
sam_move = move_finished_ids.move_dest_ids
|
||||
self.assertEqual(len(sam_move), 1)
|
||||
self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
|
||||
|
|
@ -137,24 +180,24 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
"""
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
warehouse.delivery_steps = 'pick_pack_ship'
|
||||
self.warehouse.flush_model()
|
||||
self.env.ref('stock.route_warehouse0_mto').active = True
|
||||
self.route_mto.active = True
|
||||
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
|
||||
picking_customer = self.env['stock.picking'].create({
|
||||
'location_id': self.warehouse.wh_output_stock_loc_id.id,
|
||||
'location_dest_id': self.customer_location,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'partner_id': self.env['ir.model.data']._xmlid_to_res_id('base.res_partner_4'),
|
||||
'picking_type_id': self.warehouse.out_type_id.id,
|
||||
'state': 'draft',
|
||||
})
|
||||
|
||||
self.env['stock.move'].create({
|
||||
'name': self.finished_product.name,
|
||||
'product_id': self.finished_product.id,
|
||||
'product_uom_qty': 2,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'picking_id': picking_customer.id,
|
||||
'location_id': self.warehouse.wh_output_stock_loc_id.id,
|
||||
'location_dest_id': self.customer_location,
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'procure_method': 'make_to_order',
|
||||
'origin': 'SOURCEDOCUMENT',
|
||||
'state': 'draft',
|
||||
|
|
@ -179,14 +222,12 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
]).picking_id
|
||||
|
||||
self.assertTrue(picking_stock_preprod)
|
||||
self.assertTrue(picking_stock_postprod)
|
||||
self.assertFalse(picking_stock_postprod)
|
||||
self.assertEqual(picking_stock_preprod.state, 'assigned')
|
||||
self.assertEqual(picking_stock_postprod.state, 'waiting')
|
||||
self.assertEqual(picking_stock_preprod.origin, production_order.name, 'The pre-prod origin should be the MO name')
|
||||
self.assertEqual(picking_stock_postprod.origin, 'SOURCEDOCUMENT', 'The post-prod origin should be the SO name')
|
||||
|
||||
picking_stock_preprod.action_assign()
|
||||
picking_stock_preprod.move_line_ids.qty_done = 4
|
||||
picking_stock_preprod.move_ids.write({'quantity': 4, 'picked': True})
|
||||
picking_stock_preprod._action_done()
|
||||
|
||||
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.lot_stock_id).mapped('quantity')))
|
||||
|
|
@ -194,7 +235,6 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
|
||||
production_order.action_assign()
|
||||
self.assertEqual(production_order.reservation_state, 'assigned')
|
||||
self.assertEqual(picking_stock_postprod.state, 'waiting')
|
||||
|
||||
produce_form = Form(production_order)
|
||||
produce_form.qty_producing = production_order.product_qty
|
||||
|
|
@ -203,15 +243,16 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
|
||||
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id).mapped('quantity')))
|
||||
|
||||
picking_stock_postprod = self.env['stock.move'].search([
|
||||
('product_id', '=', self.finished_product.id),
|
||||
('location_id', '=', self.warehouse.sam_loc_id.id),
|
||||
('location_dest_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('picking_type_id', '=', self.warehouse.sam_type_id.id)
|
||||
]).picking_id
|
||||
self.assertTrue(picking_stock_postprod)
|
||||
self.assertEqual(picking_stock_postprod.state, 'assigned')
|
||||
|
||||
picking_stock_pick = self.env['stock.move'].search([
|
||||
('product_id', '=', self.finished_product.id),
|
||||
('location_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('location_dest_id', '=', self.warehouse.wh_pack_stock_loc_id.id),
|
||||
('picking_type_id', '=', self.warehouse.pick_type_id.id)
|
||||
]).picking_id
|
||||
self.assertEqual(picking_stock_pick.move_ids.move_orig_ids.picking_id, picking_stock_postprod)
|
||||
self.assertEqual(picking_customer.move_ids.move_orig_ids.picking_id, picking_stock_postprod)
|
||||
|
||||
def test_cancel_propagation(self):
|
||||
""" Test cancelling moves in a 'picking before
|
||||
|
|
@ -223,18 +264,18 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
|
||||
picking_customer = self.env['stock.picking'].create({
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.customer_location,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'partner_id': self.env['ir.model.data']._xmlid_to_res_id('base.res_partner_4'),
|
||||
'picking_type_id': self.warehouse.out_type_id.id,
|
||||
'state': 'draft',
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': self.finished_product.name,
|
||||
'product_id': self.finished_product.id,
|
||||
'product_uom_qty': 2,
|
||||
'picking_id': picking_customer.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.customer_location,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_customer.action_confirm()
|
||||
|
|
@ -247,22 +288,13 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
|
||||
('picking_type_id', '=', self.warehouse.pbm_type_id.id)
|
||||
])
|
||||
move_stock_postprod = self.env['stock.move'].search([
|
||||
('product_id', '=', self.finished_product.id),
|
||||
('location_id', '=', self.warehouse.sam_loc_id.id),
|
||||
('location_dest_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('picking_type_id', '=', self.warehouse.sam_type_id.id)
|
||||
])
|
||||
|
||||
self.assertTrue(move_stock_preprod)
|
||||
self.assertTrue(move_stock_postprod)
|
||||
self.assertEqual(move_stock_preprod.state, 'assigned')
|
||||
self.assertEqual(move_stock_postprod.state, 'waiting')
|
||||
|
||||
move_stock_preprod._action_cancel()
|
||||
self.assertEqual(production_order.state, 'confirmed')
|
||||
production_order.action_cancel()
|
||||
self.assertTrue(move_stock_postprod.state, 'cancel')
|
||||
|
||||
def test_no_initial_demand(self):
|
||||
""" Test MO/picking before manufacturing/picking after manufacturing
|
||||
|
|
@ -294,7 +326,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
])
|
||||
new_product = self.env['product.product'].create({
|
||||
'name': 'New product',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
bom.consumption = 'flexible'
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
|
|
@ -332,19 +364,17 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
one_unit_uom = self.env.ref('uom.product_uom_unit')
|
||||
[two_units_uom, four_units_uom] = self.env['uom.uom'].create([{
|
||||
'name': 'x%s' % i,
|
||||
'category_id': self.ref('uom.product_uom_categ_unit'),
|
||||
'uom_type': 'bigger',
|
||||
'factor_inv': i,
|
||||
'relative_factor': i,
|
||||
'relative_uom_id': one_unit_uom.id,
|
||||
} for i in [2, 4]])
|
||||
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Super Product',
|
||||
'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))],
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
secondary_product = self.env['product.product'].create({
|
||||
'name': 'Secondary',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Component',
|
||||
|
|
@ -375,15 +405,18 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
'product_max_qty': 2,
|
||||
})
|
||||
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
self.env['stock.rule'].run_scheduler()
|
||||
mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
|
||||
pickings = mo.picking_ids
|
||||
self.assertEqual(len(pickings), 2)
|
||||
self.assertEqual(len(pickings), 1)
|
||||
|
||||
preprod_picking = pickings[0] if pickings[0].location_id == warehouse_stock_location else pickings[1]
|
||||
self.assertEqual(preprod_picking.location_id, warehouse_stock_location)
|
||||
self.assertEqual(preprod_picking.location_dest_id, pre_production_location)
|
||||
|
||||
mo.button_mark_done()
|
||||
pickings = mo.picking_ids
|
||||
self.assertEqual(len(pickings), 2)
|
||||
postprod_picking = pickings - preprod_picking
|
||||
self.assertEqual(postprod_picking.location_id, post_production_location)
|
||||
self.assertEqual(postprod_picking.location_dest_id, warehouse_stock_location)
|
||||
|
|
@ -393,23 +426,18 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
('location_id', '=', post_production_location.id),
|
||||
('location_dest_id', '=', warehouse_stock_location.id),
|
||||
])
|
||||
self.assertEqual(byproduct_postprod_move.state, 'waiting')
|
||||
self.assertEqual(byproduct_postprod_move.group_id.name, mo.name)
|
||||
self.assertEqual(byproduct_postprod_move.state, 'assigned')
|
||||
self.assertEqual(byproduct_postprod_move.reference_ids.name, mo.name)
|
||||
|
||||
def test_manufacturing_3_steps_trigger_reordering_rules(self):
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
|
||||
with Form(self.raw_product) as p:
|
||||
p.route_ids.clear()
|
||||
p.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
|
||||
|
||||
# Create an additional BoM for component
|
||||
product_form = Form(self.env['product.product'])
|
||||
product_form.name = 'Wood'
|
||||
product_form.detailed_type = 'product'
|
||||
product_form.is_storable = True
|
||||
product_form.uom_id = self.uom_unit
|
||||
product_form.uom_po_id = self.uom_unit
|
||||
self.wood_product = product_form.save()
|
||||
|
||||
# Create bom for manufactured product
|
||||
|
|
@ -435,19 +463,19 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
rr_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
rr_form.product_id = self.finished_product
|
||||
rr_form.location_id = self.warehouse.lot_stock_id
|
||||
rr_finish = rr_form.save()
|
||||
rr_form.save()
|
||||
|
||||
rr_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
rr_form.product_id = self.raw_product
|
||||
rr_form.location_id = self.warehouse.lot_stock_id
|
||||
rr_form.save()
|
||||
rr_raw = rr_form.save()
|
||||
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
self.env['stock.rule'].run_scheduler()
|
||||
|
||||
pickings_component = self.env['stock.picking'].search(
|
||||
[('product_id', '=', self.wood_product.id)])
|
||||
self.assertTrue(pickings_component)
|
||||
self.assertTrue(rr_finish.name in pickings_component.origin)
|
||||
self.assertTrue(rr_raw.name in pickings_component.origin)
|
||||
|
||||
def test_2_steps_and_additional_moves(self):
|
||||
""" Suppose a 2-steps configuration. If a user adds a product to an existing draft MO and then
|
||||
|
|
@ -466,7 +494,6 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
'location_dest_id': component_move.location_dest_id.id,
|
||||
'picking_type_id': component_move.picking_type_id.id,
|
||||
'product_id': self.product_2.id,
|
||||
'name': self.product_2.display_name,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_2.uom_id.id,
|
||||
'warehouse_id': component_move.warehouse_id.id,
|
||||
|
|
@ -478,114 +505,6 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
|
||||
self.assertEqual(self.bom.bom_line_ids.product_id + self.product_2, mo.picking_ids.move_ids.product_id)
|
||||
|
||||
def test_manufacturing_complex_product_3_steps(self):
|
||||
""" Test MO/picking after manufacturing a complex product which uses
|
||||
manufactured components. Ensure that everything is created and picked
|
||||
correctly.
|
||||
"""
|
||||
|
||||
self.warehouse.mto_pull_id.route_id.active = True
|
||||
# Creating complex product which trigger another manifacture
|
||||
|
||||
routes = self.warehouse.manufacture_pull_id.route_id + self.warehouse.mto_pull_id.route_id
|
||||
self.complex_product = self.env['product.product'].create({
|
||||
'name': 'Arrow',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, routes.ids)],
|
||||
})
|
||||
|
||||
# Create raw product for manufactured product
|
||||
self.raw_product_2 = self.env['product.product'].create({
|
||||
'name': 'Raw Iron',
|
||||
'type': 'product',
|
||||
'uom_id': self.uom_unit.id,
|
||||
'uom_po_id': self.uom_unit.id,
|
||||
})
|
||||
|
||||
self.finished_product.route_ids = [(6, 0, routes.ids)]
|
||||
|
||||
# Create bom for manufactured product
|
||||
bom_product_form = Form(self.env['mrp.bom'])
|
||||
bom_product_form.product_id = self.complex_product
|
||||
bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id
|
||||
with bom_product_form.bom_line_ids.new() as line:
|
||||
line.product_id = self.finished_product
|
||||
line.product_qty = 1.0
|
||||
with bom_product_form.bom_line_ids.new() as line:
|
||||
line.product_id = self.raw_product_2
|
||||
line.product_qty = 1.0
|
||||
|
||||
self.complex_bom = bom_product_form.save()
|
||||
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.complex_product
|
||||
production_form.picking_type_id = self.warehouse.manu_type_id
|
||||
production = production_form.save()
|
||||
production.action_confirm()
|
||||
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 2)
|
||||
sfp_move_raw_id, raw_move_raw_id = move_raw_ids
|
||||
self.assertEqual(sfp_move_raw_id.product_id, self.finished_product)
|
||||
self.assertEqual(raw_move_raw_id.product_id, self.raw_product_2)
|
||||
|
||||
for move_raw_id in move_raw_ids:
|
||||
self.assertEqual(move_raw_id.picking_type_id, self.warehouse.manu_type_id)
|
||||
|
||||
pbm_move = move_raw_id.move_orig_ids
|
||||
self.assertEqual(len(pbm_move), 1)
|
||||
self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
|
||||
self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
|
||||
self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
|
||||
|
||||
# Check move locations
|
||||
move_finished_ids = production.move_finished_ids
|
||||
self.assertEqual(len(move_finished_ids), 1)
|
||||
self.assertEqual(move_finished_ids.product_id, self.complex_product)
|
||||
self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
|
||||
sam_move = move_finished_ids.move_dest_ids
|
||||
self.assertEqual(len(sam_move), 1)
|
||||
self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
|
||||
self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
|
||||
self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
|
||||
self.assertFalse(sam_move.move_dest_ids)
|
||||
|
||||
subproduction = self.env['mrp.production'].browse(production.id+1)
|
||||
sfp_pickings = subproduction.picking_ids.sorted('id')
|
||||
|
||||
# SFP Production: 2 pickings, 1 group
|
||||
self.assertEqual(len(sfp_pickings), 2)
|
||||
self.assertEqual(sfp_pickings.mapped('group_id'), subproduction.procurement_group_id)
|
||||
|
||||
# Move Raw Stick - Stock -> Preprocessing
|
||||
picking = sfp_pickings[0]
|
||||
self.assertEqual(len(picking.move_ids), 1)
|
||||
picking.move_ids[0].product_id = self.raw_product
|
||||
|
||||
# Move SFP - PostProcessing -> Stock
|
||||
picking = sfp_pickings[1]
|
||||
self.assertEqual(len(picking.move_ids), 1)
|
||||
picking.move_ids[0].product_id = self.finished_product
|
||||
|
||||
# Main production 2 pickings, 1 group
|
||||
pickings = production.picking_ids.sorted('id')
|
||||
self.assertEqual(len(pickings), 2)
|
||||
self.assertEqual(pickings.mapped('group_id'), production.procurement_group_id)
|
||||
|
||||
# Move 2 components Stock -> Preprocessing
|
||||
picking = pickings[0]
|
||||
self.assertEqual(len(picking.move_ids), 2)
|
||||
picking.move_ids[0].product_id = self.finished_product
|
||||
picking.move_ids[1].product_id = self.raw_product_2
|
||||
|
||||
# Move FP PostProcessing -> Stock
|
||||
picking = pickings[1]
|
||||
self.assertEqual(len(picking.move_ids), 1)
|
||||
picking.product_id = self.complex_product
|
||||
|
||||
def test_child_parent_relationship_on_backorder_creation(self):
|
||||
""" Test Child Mo and Source Mo in 2/3-step production for reorder
|
||||
rules in backorder using order points with the help of run scheduler """
|
||||
|
|
@ -599,7 +518,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
rr_form.product_max_qty = 40
|
||||
rr_form.save()
|
||||
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
self.env['stock.rule'].run_scheduler()
|
||||
|
||||
mo = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
|
||||
mo_form = Form(mo)
|
||||
|
|
@ -667,7 +586,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
finished_product = self.env['product.product'].create({
|
||||
'name': 'Product',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'route_ids': manufacturing_route,
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
|
|
@ -690,8 +609,286 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
|||
'route_id': manufacturing_route.id,
|
||||
'bom_id': bom_2.id,
|
||||
})
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
self.env['stock.rule'].run_scheduler()
|
||||
mo = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
self.assertEqual(mo.product_qty, 1.0)
|
||||
self.assertEqual(mo.bom_id, bom_2)
|
||||
|
||||
# def test_manufacturing_bom_with_repetitions(self):
|
||||
# """
|
||||
# Checks that manufacturing orders created to manufacture the components of a BOM
|
||||
# are set with the correct quantities when products appear with repetitions.
|
||||
# - Create 5 products: product 1,2,3,4 (P1,P2,P3 and P4) and a final product (FP)
|
||||
# - Set routes to manifacture on each product
|
||||
# - For P1, P2, P3, P4 add a 0:0 reordering rule.
|
||||
# - Add a BOM for P2 with 1 unit of P1 as components
|
||||
# - Add a BOM for P3 with 1 unit of P2 as components
|
||||
# - Add a BOM for P4 with 1 unit of P3 as components
|
||||
# - Add a BOM for FP with 3 unit of P4 and 2 units of P3 as components
|
||||
# """
|
||||
# manufacturing_route = self.env['stock.rule'].search([
|
||||
# ('action', '=', 'manufacture')]).route_id
|
||||
# products = self.env['product.product'].create([
|
||||
# {
|
||||
# 'name': 'FP',
|
||||
# 'is_storable': True,
|
||||
# 'route_ids': manufacturing_route,
|
||||
# },
|
||||
# {
|
||||
# 'name': 'P1',
|
||||
# 'is_storable': True,
|
||||
# 'route_ids': manufacturing_route,
|
||||
# },
|
||||
# {
|
||||
# 'name': 'P2',
|
||||
# 'is_storable': True,
|
||||
# 'route_ids': manufacturing_route,
|
||||
# },
|
||||
# {
|
||||
# 'name': 'P3',
|
||||
# 'is_storable': True,
|
||||
# 'route_ids': manufacturing_route,
|
||||
# },
|
||||
# {
|
||||
# 'name': 'P4',
|
||||
# 'is_storable': True,
|
||||
# 'route_ids': manufacturing_route,
|
||||
# },
|
||||
#
|
||||
# ])
|
||||
# self.env['stock.warehouse.orderpoint'].create([
|
||||
# {
|
||||
# 'name': 'My orderpoint',
|
||||
# 'product_id': i,
|
||||
# 'product_min_qty': 0,
|
||||
# 'product_max_qty': 0,
|
||||
# } for i in products.ids[1:]
|
||||
# ])
|
||||
# self.env['mrp.bom'].create([
|
||||
# {
|
||||
# 'product_tmpl_id': products[2].product_tmpl_id.id,
|
||||
# 'product_qty': 1,
|
||||
# 'product_uom_id': products[2].uom_id.id,
|
||||
# 'type': 'normal',
|
||||
# 'bom_line_ids': [
|
||||
# Command.create({
|
||||
# 'product_id': products[1].id,
|
||||
# 'product_qty': 1,
|
||||
# })
|
||||
# ]},
|
||||
# {
|
||||
# 'product_tmpl_id': products[3].product_tmpl_id.id,
|
||||
# 'product_qty': 1,
|
||||
# 'product_uom_id': products[3].uom_id.id,
|
||||
# 'type': 'normal',
|
||||
# 'bom_line_ids': [
|
||||
# Command.create({
|
||||
# 'product_id': products[2].id,
|
||||
# 'product_qty': 1,
|
||||
# })
|
||||
# ]},
|
||||
# {
|
||||
# 'product_tmpl_id': products[4].product_tmpl_id.id,
|
||||
# 'product_qty': 1,
|
||||
# 'product_uom_id': products[4].uom_id.id,
|
||||
# 'type': 'normal',
|
||||
# 'bom_line_ids': [
|
||||
# Command.create({
|
||||
# 'product_id': products[3].id,
|
||||
# 'product_qty': 1,
|
||||
# })
|
||||
# ]},
|
||||
# {
|
||||
# 'product_tmpl_id': products[0].product_tmpl_id.id,
|
||||
# 'product_qty': 1,
|
||||
# 'product_uom_id': products[0].uom_id.id,
|
||||
# 'type': 'normal',
|
||||
# 'bom_line_ids': [
|
||||
# Command.create({
|
||||
# 'product_id': products[4].id,
|
||||
# 'product_qty': 3,
|
||||
# }),
|
||||
# Command.create({
|
||||
# 'product_id': products[2].id,
|
||||
# 'product_qty': 2,
|
||||
# }),
|
||||
# ]},
|
||||
# ])
|
||||
# mo = self.env['mrp.production'].create({
|
||||
# 'product_id': products[0].id,
|
||||
# 'product_uom_qty': 1,
|
||||
# })
|
||||
# mo.action_confirm()
|
||||
# mo_P1 = self.env['mrp.production'].search([('product_id', '=', products[1].id)])
|
||||
# mo_P2 = self.env['mrp.production'].search([('product_id', '=', products[2].id)])
|
||||
# self.assertEqual(mo_P1.product_uom_qty, 5.0)
|
||||
# self.assertEqual(mo_P2.product_uom_qty, 5.0)
|
||||
#
|
||||
def test_update_component_qty(self):
|
||||
self.warehouse.manufacture_steps = "pbm"
|
||||
component = self.bom.bom_line_ids.product_id
|
||||
mo = self.env['mrp.production'].create({
|
||||
'product_id': self.bom.product_id.id,
|
||||
'bom_id': self.bom.id,
|
||||
'product_qty': 1,
|
||||
'location_src_id': self.warehouse.pbm_loc_id.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.move_raw_ids.product_uom_qty, 2.0)
|
||||
self.assertEqual(mo.picking_ids.move_ids.product_uom_qty, 2.0)
|
||||
# we require a more components to complete the MO
|
||||
mo_form = Form(mo)
|
||||
with mo_form.move_raw_ids.new() as raw_move:
|
||||
raw_move.product_id = component
|
||||
raw_move.product_uom_qty = 1.0
|
||||
mo = mo_form.save()
|
||||
# check that the related moves qty is correctly updated
|
||||
self.assertEqual(mo.move_raw_ids.product_uom_qty, 3.0)
|
||||
self.assertEqual(mo.picking_ids.move_ids.product_uom_qty, 3.0)
|
||||
|
||||
def test_component_and_byproduct_on_transfers(self):
|
||||
"""
|
||||
Checks if transfers is updated when we adding a new byproduct/component
|
||||
after confirm the MO
|
||||
"""
|
||||
self.env.user.group_ids += self.env.ref('mrp.group_mrp_byproducts')
|
||||
demo = self.env['product.product'].create({
|
||||
'name': 'DEMO',
|
||||
'is_storable': True,
|
||||
})
|
||||
comp1 = self.env['product.product'].create({
|
||||
'name': 'COMP1'
|
||||
})
|
||||
comp2 = self.env['product.product'].create({
|
||||
'name': 'COMP2'
|
||||
})
|
||||
bprod1 = self.env['product.product'].create({
|
||||
'name': 'BPROD1'
|
||||
})
|
||||
bprod2 = self.env['product.product'].create({
|
||||
'name': 'BPROD2'
|
||||
})
|
||||
|
||||
warehouse = self.warehouse
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
warehouse_stock_location = warehouse.lot_stock_id
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': demo.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': comp1.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
'byproduct_ids': [(0, 0, {
|
||||
'product_id': bprod1.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
})
|
||||
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'warehouse_id': warehouse.id,
|
||||
'location_id': warehouse_stock_location.id,
|
||||
'product_id': demo.id,
|
||||
'product_min_qty': 2,
|
||||
'product_max_qty': 2,
|
||||
})
|
||||
|
||||
self.env['stock.rule'].run_scheduler()
|
||||
mo = self.env['mrp.production'].search([('product_id', '=', demo.id)])
|
||||
mo.action_confirm()
|
||||
|
||||
mo_form = Form(mo)
|
||||
with mo_form.move_raw_ids.new() as raw_move:
|
||||
raw_move.product_id = comp2
|
||||
raw_move.product_uom_qty = 1.0
|
||||
with mo_form.move_byproduct_ids.new() as byprod_move:
|
||||
byprod_move.product_id = bprod2
|
||||
byprod_move.quantity = 1.0
|
||||
mo = mo_form.save()
|
||||
mo.with_context({'skip_consumption': True}).button_mark_done()
|
||||
|
||||
self.assertEqual(len(mo.picking_ids), 2, "Should have 2 pickings: Components + (Final product and byproducts)")
|
||||
for picking in mo.picking_ids:
|
||||
if demo in [m.product_id for m in picking.move_ids]:
|
||||
self.assertEqual(len(picking.move_ids), 3, "Should have 3 moves for: Demo, Bprod1 and Bprod2")
|
||||
self.assertEqual([move.product_id for move in picking.move_ids.sorted(lambda m: bool(m.product_id))], [demo, bprod1, bprod2])
|
||||
else:
|
||||
self.assertEqual(len(picking.move_ids), 2, "Should have 2 moves for: Comp1 and Comp2")
|
||||
self.assertEqual([move.product_id for move in picking.move_ids.sorted(lambda m: bool(m.product_id))], [comp1, comp2])
|
||||
|
||||
def test_pick_components_uses_shipping_policy_from_picking_type(self):
|
||||
self.warehouse.manufacture_steps = "pbm"
|
||||
pick_components_type = self.warehouse.pbm_type_id
|
||||
|
||||
for move_type in ["direct", "one"]:
|
||||
pick_components_type.move_type = move_type
|
||||
|
||||
mo = self.env["mrp.production"].create({
|
||||
"bom_id": self.bom.id,
|
||||
"location_src_id": self.warehouse.pbm_loc_id.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
|
||||
self.assertEqual(mo.picking_ids[0].picking_type_id, pick_components_type)
|
||||
self.assertEqual(mo.picking_ids[0].move_type, move_type)
|
||||
|
||||
def test_update_mo_from_bom_forecast(self):
|
||||
self.warehouse_1.manufacture_steps = 'pbm_sam'
|
||||
self.env['stock.quant']._update_available_quantity(self.product_2, self.warehouse_1.lot_stock_id, 10)
|
||||
self.env['stock.quant']._update_available_quantity(self.product_3, self.warehouse_1.lot_stock_id, 20)
|
||||
mo = self.env['mrp.production'].create({
|
||||
'bom_id': self.bom_1.id,
|
||||
'picking_type_id': self.warehouse_1.manu_type_id.id,
|
||||
})
|
||||
mo.action_confirm()
|
||||
self.assertEqual(self.product_1.virtual_available, -4)
|
||||
self.assertEqual(self.product_2.virtual_available, 8)
|
||||
rr = self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'John Cutter RR',
|
||||
'product_id': self.product_1.id,
|
||||
'warehouse_id': mo.warehouse_id.id,
|
||||
})
|
||||
self.assertEqual(rr.qty_forecast, -4)
|
||||
# Update the BoM
|
||||
self.bom_1.bom_line_ids[1].unlink()
|
||||
self.bom_1.bom_line_ids[0].product_qty = 3
|
||||
self.env['mrp.bom.line'].create({
|
||||
'product_id': self.product_3.id,
|
||||
'product_qty': 5,
|
||||
'bom_id': self.bom_1.id,
|
||||
})
|
||||
mo.action_update_bom()
|
||||
self.assertEqual(rr.qty_forecast, 0)
|
||||
self.assertEqual(self.product_1.virtual_available, 0)
|
||||
self.assertEqual(self.product_2.virtual_available, 7)
|
||||
self.assertEqual(self.product_3.virtual_available, 15)
|
||||
pre_prod_pick = mo.picking_ids.filtered(lambda p: p.picking_type_id == mo.warehouse_id.pbm_type_id)
|
||||
self.assertRecordValues(pre_prod_pick.move_ids, [
|
||||
{'product_id': self.product_2.id, 'product_uom_qty': 3, 'product_qty_available': 10},
|
||||
{'product_id': self.product_1.id, 'product_uom_qty': 0, 'product_qty_available': 0},
|
||||
{'product_id': self.product_3.id, 'product_uom_qty': 5, 'product_qty_available': 20},
|
||||
])
|
||||
|
||||
def test_3_steps_manufacturing_forecast(self):
|
||||
"""Check that a confirmed MO influence the forecast of the warehouse stock"""
|
||||
self.warehouse_1.manufacture_steps = 'pbm_sam'
|
||||
lovely_product = self.bom_1.product_id.copy({'uom_id': self.uom_unit.id})
|
||||
self.bom_1.product_id = lovely_product
|
||||
self.assertEqual(lovely_product.with_context(location_id=self.warehouse_1.lot_stock_id.id).virtual_available, 0.0)
|
||||
mo = self.env['mrp.production'].create({
|
||||
'bom_id': self.bom_1.id,
|
||||
'picking_type_id': self.warehouse_1.manu_type_id.id,
|
||||
'product_qty': 3.0,
|
||||
})
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
self.assertEqual(lovely_product.with_context(location_id=self.warehouse_1.lot_stock_id.id).virtual_available, 3.0)
|
||||
|
||||
def test_manufacture_to_resupply_unchecks_and_unlinks_warehouse(self):
|
||||
"""Unchecking Manufacture to Resupply should keep manufacture_to_resupply disabled."""
|
||||
manufacture_route = self.warehouse.manufacture_pull_id.route_id
|
||||
self.warehouse.manufacture_to_resupply = False
|
||||
self.assertFalse(self.warehouse.manufacture_to_resupply)
|
||||
self.assertNotIn(self.warehouse, manufacture_route.warehouse_ids)
|
||||
|
|
|
|||
56
odoo-bringout-oca-ocb-mrp/mrp/tests/test_workcenter.py
Normal file
56
odoo-bringout-oca-ocb-mrp/mrp/tests/test_workcenter.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import timedelta, datetime
|
||||
from freezegun import freeze_time
|
||||
|
||||
from . import common
|
||||
from odoo import Command
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestWorkcenterOverview(common.TestMrpCommon):
|
||||
|
||||
@freeze_time('2020-03-13') # Friday
|
||||
def test_workcenter_graph_data(self):
|
||||
fake_bom = self.env['mrp.bom'].create({
|
||||
'product_id': self.product_2.id,
|
||||
'product_tmpl_id': self.product_2.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
Command.create({
|
||||
'name': 'Make it look you are working',
|
||||
'workcenter_id': self.workcenter_2.id,
|
||||
'time_cycle_manual': 60, 'sequence': 1})
|
||||
],
|
||||
'type': 'normal',
|
||||
})
|
||||
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
lang.week_start = '3' # Wednesday
|
||||
week_range, date_start, date_stop = self.workcenter_2._get_week_range_and_first_last_days()
|
||||
self.assertEqual(next(iter(week_range)), date_start)
|
||||
self.assertEqual(date_stop.strftime('%Y-%m-%d'), '2020-04-07')
|
||||
self.assertEqual(list(week_range.items())[2][1], '18 - 24 Mar')
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_2
|
||||
mo_form.bom_id = fake_bom
|
||||
mo_form.product_qty = 20
|
||||
mo_form.save().action_confirm()
|
||||
|
||||
mo_form_2 = Form(self.env['mrp.production'])
|
||||
mo_form_2.product_id = self.product_2
|
||||
mo_form_2.bom_id = fake_bom
|
||||
mo_form_2.product_qty = 60
|
||||
mo_form_2.date_start = datetime.today() + timedelta(weeks=1)
|
||||
mo_form_2.save().action_confirm()
|
||||
|
||||
wc_load_data = self.workcenter_2._get_workcenter_load_per_week(week_range, date_start, date_stop)
|
||||
self.assertListEqual(list(wc_load_data[self.workcenter_2].values()), [20.0, 60.0])
|
||||
self.assertListEqual(list(wc_load_data[self.workcenter_2].keys()), [datetime(2020, 3, 11), datetime(2020, 3, 18)])
|
||||
load_graph_data = self.workcenter_2._prepare_graph_data(wc_load_data, week_range)
|
||||
self.assertEqual(load_graph_data[self.workcenter_2.id][0]['is_sample_data'], False)
|
||||
self.assertListEqual(load_graph_data[self.workcenter_2.id][0]['labels'], list(week_range.values()))
|
||||
self.assertListEqual(load_graph_data[self.workcenter_2.id][0]['values'], [[0, 20.0, 40.0, 0, 0], 40.0, [0, 0, 20.0, 0, 0]])
|
||||
Loading…
Add table
Add a link
Reference in a new issue