diff options
Diffstat (limited to 'addons/sale_mrp/tests')
| -rw-r--r-- | addons/sale_mrp/tests/__init__.py | 8 | ||||
| -rw-r--r-- | addons/sale_mrp/tests/test_multistep_manufacturing.py | 129 | ||||
| -rw-r--r-- | addons/sale_mrp/tests/test_sale_mrp_flow.py | 1644 | ||||
| -rw-r--r-- | addons/sale_mrp/tests/test_sale_mrp_kit_bom.py | 166 | ||||
| -rw-r--r-- | addons/sale_mrp/tests/test_sale_mrp_lead_time.py | 162 | ||||
| -rw-r--r-- | addons/sale_mrp/tests/test_sale_mrp_procurement.py | 172 |
6 files changed, 2281 insertions, 0 deletions
diff --git a/addons/sale_mrp/tests/__init__.py b/addons/sale_mrp/tests/__init__.py new file mode 100644 index 00000000..56a3cc3f --- /dev/null +++ b/addons/sale_mrp/tests/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_sale_mrp_flow +from . import test_sale_mrp_kit_bom +from . import test_sale_mrp_lead_time +from . import test_sale_mrp_procurement +from . import test_multistep_manufacturing
\ No newline at end of file diff --git a/addons/sale_mrp/tests/test_multistep_manufacturing.py b/addons/sale_mrp/tests/test_multistep_manufacturing.py new file mode 100644 index 00000000..b0ab1d09 --- /dev/null +++ b/addons/sale_mrp/tests/test_multistep_manufacturing.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import Form +from odoo.addons.mrp.tests.common import TestMrpCommon + + +class TestMultistepManufacturing(TestMrpCommon): + + def setUp(self): + super(TestMultistepManufacturing, self).setUp() + + self.env.ref('stock.route_warehouse0_mto').active = True + self.MrpProduction = self.env['mrp.production'] + # Create warehouse + warehouse_form = Form(self.env['stock.warehouse']) + warehouse_form.name = 'Test' + warehouse_form.code = 'Test' + self.warehouse = warehouse_form.save() + + self.uom_unit = self.env.ref('uom.product_uom_unit') + + # Create manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Stick' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + product_form.route_ids.clear() + product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id) + product_form.route_ids.add(self.warehouse.mto_pull_id.route_id) + self.product_manu = product_form.save() + + # Create raw product for manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Raw Stick' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + self.product_raw = product_form.save() + + # Create bom for manufactured product + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.product_manu + bom_product_form.product_tmpl_id = self.product_manu.product_tmpl_id + bom_product_form.product_qty = 1.0 + bom_product_form.type = 'normal' + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.product_raw + bom_line.product_qty = 2.0 + self.bom_prod_manu = bom_product_form.save() + + # Create sale order + sale_form = Form(self.env['sale.order']) + sale_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + sale_form.picking_policy = 'direct' + sale_form.warehouse_id = self.warehouse + with sale_form.order_line.new() as line: + line.name = self.product_manu.name + line.product_id = self.product_manu + line.product_uom_qty = 1.0 + line.product_uom = self.uom_unit + line.price_unit = 10.0 + self.sale_order = sale_form.save() + + def test_00_manufacturing_step_one(self): + """ Testing for Step-1 """ + # Change steps of manufacturing. + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'mrp_one_step' + # Confirm sale order. + self.sale_order.action_confirm() + # Check all procurements for created sale order + mo_procurement = self.MrpProduction.search([('origin', '=', self.sale_order.name)]) + # Get manufactured procurement + self.assertEqual(mo_procurement.location_src_id.id, self.warehouse.lot_stock_id.id, "Source loction does not match.") + self.assertEqual(mo_procurement.location_dest_id.id, self.warehouse.lot_stock_id.id, "Destination location does not match.") + self.assertEqual(len(mo_procurement), 1, "No Procurement !") + + def test_01_manufacturing_step_two(self): + """ Testing for Step-2 """ + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm' + self.sale_order.action_confirm() + # Get manufactured procurement + mo_procurement = self.MrpProduction.search([('origin', '=', self.sale_order.name)]) + self.assertEqual(mo_procurement.location_src_id.id, self.warehouse.pbm_loc_id.id, "Source loction does not match.") + self.assertEqual(mo_procurement.location_dest_id.id, self.warehouse.lot_stock_id.id, "Destination location does not match.") + + self.assertEqual(len(mo_procurement), 1, "No Procurement !") + + def test_cancel_multilevel_manufacturing(self): + """ Testing for multilevel Manufacturing orders. + When user creates multi-level manufacturing orders, + and then cancelles child manufacturing order, + an activity should be generated on parent MO, to notify user that + demands from child MO has been cancelled. + """ + + product_form = Form(self.env['product.product']) + product_form.name = 'Screw' + self.product_screw = product_form.save() + + # Add routes for manufacturing and make to order to the raw material product + with Form(self.product_raw) as p1: + p1.route_ids.clear() + p1.route_ids.add(self.warehouse_1.manufacture_pull_id.route_id) + p1.route_ids.add(self.warehouse_1.mto_pull_id.route_id) + + # New BoM for raw material product, it will generate another Production order i.e. child Production order + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.product_raw + bom_product_form.product_tmpl_id = self.product_raw.product_tmpl_id + bom_product_form.product_qty = 1.0 + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.product_screw + bom_line.product_qty = 5.0 + self.bom_prod_manu = bom_product_form.save() + + # create MO from sale order. + self.sale_order.action_confirm() + # Find child MO. + child_manufaturing = self.env['mrp.production'].search([('product_id', '=', self.product_raw.id)]) + self.assertTrue((len(child_manufaturing.ids) == 1), 'Manufacturing order of raw material must be generated.') + # Cancel child MO. + child_manufaturing.action_cancel() + manufaturing_from_so = self.env['mrp.production'].search([('product_id', '=', self.product_manu.id)]) + # Check if activity is generated or not on parent MO. + exception = self.env['mail.activity'].search([('res_model', '=', 'mrp.production'), + ('res_id', '=', manufaturing_from_so.id)]) + self.assertEqual(len(exception.ids), 1, 'When user cancelled child manufacturing, exception must be generated on parent manufacturing.') diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py new file mode 100644 index 00000000..7534c2f0 --- /dev/null +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -0,0 +1,1644 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon +from odoo.tests import common, Form +from odoo.exceptions import UserError +from odoo.tools import mute_logger, float_compare + + +# these tests create accounting entries, and therefore need a chart of accounts +@common.tagged('post_install', '-at_install') +class TestSaleMrpFlow(ValuationReconciliationTestCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.env.ref('stock.route_warehouse0_mto').active = True + + # Useful models + cls.StockMove = cls.env['stock.move'] + cls.UoM = cls.env['uom.uom'] + cls.MrpProduction = cls.env['mrp.production'] + cls.Inventory = cls.env['stock.inventory'] + cls.InventoryLine = cls.env['stock.inventory.line'] + cls.ProductCategory = cls.env['product.category'] + + cls.categ_unit = cls.env.ref('uom.product_uom_categ_unit') + cls.categ_kgm = cls.env.ref('uom.product_uom_categ_kgm') + + cls.uom_kg = cls.env['uom.uom'].search([('category_id', '=', cls.categ_kgm.id), ('uom_type', '=', 'reference')], limit=1) + cls.uom_kg.write({ + 'name': 'Test-KG', + 'rounding': 0.000001}) + cls.uom_gm = cls.UoM.create({ + 'name': 'Test-G', + 'category_id': cls.categ_kgm.id, + 'uom_type': 'smaller', + 'factor': 1000.0, + 'rounding': 0.001}) + cls.uom_unit = cls.env['uom.uom'].search([('category_id', '=', cls.categ_unit.id), ('uom_type', '=', 'reference')], limit=1) + cls.uom_unit.write({ + 'name': 'Test-Unit', + 'rounding': 0.01}) + cls.uom_dozen = cls.UoM.create({ + 'name': 'Test-DozenA', + 'category_id': cls.categ_unit.id, + 'factor_inv': 12, + 'uom_type': 'bigger', + 'rounding': 0.001}) + + # Creating all components + cls.component_a = cls._cls_create_product('Comp A', cls.uom_unit) + cls.component_b = cls._cls_create_product('Comp B', cls.uom_unit) + cls.component_c = cls._cls_create_product('Comp C', cls.uom_unit) + cls.component_d = cls._cls_create_product('Comp D', cls.uom_unit) + cls.component_e = cls._cls_create_product('Comp E', cls.uom_unit) + cls.component_f = cls._cls_create_product('Comp F', cls.uom_unit) + cls.component_g = cls._cls_create_product('Comp G', cls.uom_unit) + + # Create a kit 'kit_1' : + # ----------------------- + # + # kit_1 --|- component_a x2 + # |- component_b x1 + # |- component_c x3 + + cls.kit_1 = cls._cls_create_product('Kit 1', cls.uom_unit) + + cls.bom_kit_1 = cls.env['mrp.bom'].create({ + 'product_tmpl_id': cls.kit_1.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine = cls.env['mrp.bom.line'] + BomLine.create({ + 'product_id': cls.component_a.id, + 'product_qty': 2.0, + 'bom_id': cls.bom_kit_1.id}) + BomLine.create({ + 'product_id': cls.component_b.id, + 'product_qty': 1.0, + 'bom_id': cls.bom_kit_1.id}) + BomLine.create({ + 'product_id': cls.component_c.id, + 'product_qty': 3.0, + 'bom_id': cls.bom_kit_1.id}) + + # Create a kit 'kit_parent' : + # --------------------------- + # + # kit_parent --|- kit_2 x2 --|- component_d x1 + # | |- kit_1 x2 -------|- component_a x2 + # | |- component_b x1 + # | |- component_c x3 + # | + # |- kit_3 x1 --|- component_f x1 + # | |- component_g x2 + # | + # |- component_e x1 + + # Creating all kits + cls.kit_2 = cls._cls_create_product('Kit 2', cls.uom_unit) + cls.kit_3 = cls._cls_create_product('kit 3', cls.uom_unit) + cls.kit_parent = cls._cls_create_product('Kit Parent', cls.uom_unit) + + # Linking the kits and the components via some 'phantom' BoMs + bom_kit_2 = cls.env['mrp.bom'].create({ + 'product_tmpl_id': cls.kit_2.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine.create({ + 'product_id': cls.component_d.id, + 'product_qty': 1.0, + 'bom_id': bom_kit_2.id}) + BomLine.create({ + 'product_id': cls.kit_1.id, + 'product_qty': 2.0, + 'bom_id': bom_kit_2.id}) + + bom_kit_parent = cls.env['mrp.bom'].create({ + 'product_tmpl_id': cls.kit_parent.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine.create({ + 'product_id': cls.component_e.id, + 'product_qty': 1.0, + 'bom_id': bom_kit_parent.id}) + BomLine.create({ + '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': cls.kit_3.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine.create({ + 'product_id': cls.component_f.id, + 'product_qty': 1.0, + 'bom_id': bom_kit_3.id}) + BomLine.create({ + 'product_id': cls.component_g.id, + 'product_qty': 2.0, + 'bom_id': bom_kit_3.id}) + + BomLine.create({ + 'product_id': cls.kit_3.id, + 'product_qty': 2.0, + 'bom_id': bom_kit_parent.id}) + + @classmethod + def _cls_create_product(cls, name, uom_id, routes=()): + p = Form(cls.env['product.product']) + p.name = name + p.type = 'product' + p.uom_id = uom_id + p.uom_po_id = uom_id + p.route_ids.clear() + for r in routes: + p.route_ids.add(r) + return p.save() + + def _create_product(self, name, uom_id, routes=()): + p = Form(self.env['product.product']) + p.name = name + p.type = 'product' + p.uom_id = uom_id + p.uom_po_id = uom_id + p.route_ids.clear() + for r in routes: + p.route_ids.add(r) + return p.save() + + # Helper to process quantities based on a dict following this structure : + # + # qty_to_process = { + # product_id: qty + # } + + def _process_quantities(self, moves, quantities_to_process): + """ Helper to process quantities based on a dict following this structure : + qty_to_process = { + product_id: qty + } + """ + moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys()) + for move in moves_to_process: + move.write({'quantity_done': quantities_to_process[move.product_id]}) + + def _assert_quantities(self, moves, quantities_to_process): + """ Helper to check expected quantities based on a dict following this structure : + qty_to_process = { + product_id: qty + ... + } + """ + moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys()) + for move in moves_to_process: + self.assertEqual(move.product_uom_qty, quantities_to_process[move.product_id]) + + def _create_move_quantities(self, qty_to_process, components, warehouse): + """ Helper to creates moves in order to update the quantities of components + on a specific warehouse. This ensure that all compute fields are triggered. + The structure of qty_to_process should be the following : + + qty_to_process = { + component: (qty, uom), + ... + } + """ + for comp in components: + f = Form(self.env['stock.move']) + f.name = 'Test Receipt Components' + f.location_id = self.env.ref('stock.stock_location_suppliers') + f.location_dest_id = warehouse.lot_stock_id + f.product_id = comp + f.product_uom = qty_to_process[comp][1] + f.product_uom_qty = qty_to_process[comp][0] + move = f.save() + move._action_confirm() + move._action_assign() + move_line = move.move_line_ids[0] + move_line.qty_done = qty_to_process[comp][0] + move._action_done() + + def test_00_sale_mrp_flow(self): + """ Test sale to mrp flow with diffrent unit of measure.""" + + + # Create product A, B, C, D. + # -------------------------- + route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id + route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id + product_a = self._create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto]) + product_c = self._create_product('Product C', self.uom_kg) + product_b = self._create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto]) + product_d = self._create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto]) + + # ------------------------------------------------------------------------------------------ + # Bill of materials for product A, B, D. + # ------------------------------------------------------------------------------------------ + + # Bill of materials for Product A. + with Form(self.env['mrp.bom']) as f: + f.product_tmpl_id = product_a.product_tmpl_id + f.product_qty = 2 + f.product_uom_id = self.uom_dozen + with f.bom_line_ids.new() as line: + line.product_id = product_b + line.product_qty = 3 + line.product_uom_id = self.uom_unit + with f.bom_line_ids.new() as line: + line.product_id = product_c + line.product_qty = 300.5 + line.product_uom_id = self.uom_gm + with f.bom_line_ids.new() as line: + line.product_id = product_d + line.product_qty = 4 + line.product_uom_id = self.uom_unit + + # Bill of materials for Product B. + with Form(self.env['mrp.bom']) as f: + f.product_tmpl_id = product_b.product_tmpl_id + f.product_qty = 1 + f.product_uom_id = self.uom_unit + f.type = 'phantom' + with f.bom_line_ids.new() as line: + line.product_id = product_c + line.product_qty = 0.400 + line.product_uom_id = self.uom_kg + + # Bill of materials for Product D. + with Form(self.env['mrp.bom']) as f: + f.product_tmpl_id = product_d.product_tmpl_id + f.product_qty = 1 + f.product_uom_id = self.uom_unit + with f.bom_line_ids.new() as line: + line.product_id = product_c + line.product_qty = 1 + line.product_uom_id = self.uom_kg + + # ---------------------------------------- + # Create sales order of 10 Dozen product A. + # ---------------------------------------- + + order_form = Form(self.env['sale.order']) + order_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + with order_form.order_line.new() as line: + line.product_id = product_a + line.product_uom = self.uom_dozen + line.product_uom_qty = 10 + order = order_form.save() + order.action_confirm() + + # =============================================================================== + # Sales order of 10 Dozen product A should create production order + # like .. + # =============================================================================== + # Product A 10 Dozen. + # Product C 6 kg + # As product B phantom in bom A, product A will consume product C + # ================================================================ + # For 1 unit product B it will consume 400 gm + # then for 15 unit (Product B 3 unit per 2 Dozen product A) + # product B it will consume [ 6 kg ] product C) + # Product A will consume 6 kg product C. + # + # [15 * 400 gm ( 6 kg product C)] = 6 kg product C + # + # Product C 1502.5 gm. + # [ + # For 2 Dozen product A will consume 300.5 gm product C + # then for 10 Dozen product A will consume 1502.5 gm product C. + # ] + # + # product D 20 Unit. + # [ + # For 2 dozen product A will consume 4 unit product D + # then for 10 Dozen product A will consume 20 unit of product D. + # ] + # -------------------------------------------------------------------------------- + + # <><><><><><><><><><><><><><><><><><><><> + # Check manufacturing order for product A. + # <><><><><><><><><><><><><><><><><><><><> + + # Check quantity, unit of measure and state of manufacturing order. + # ----------------------------------------------------------------- + self.env['procurement.group'].run_scheduler() + mnf_product_a = self.env['mrp.production'].search([('product_id', '=', product_a.id)]) + + self.assertTrue(mnf_product_a, 'Manufacturing order not created.') + self.assertEqual(mnf_product_a.product_qty, 120, 'Wrong product quantity in manufacturing order.') + self.assertEqual(mnf_product_a.product_uom_id, self.uom_unit, 'Wrong unit of measure in manufacturing order.') + self.assertEqual(mnf_product_a.state, 'confirmed', 'Manufacturing order should be confirmed.') + + # ------------------------------------------------------------------------------------------ + # Check 'To consume line' for production order of product A. + # ------------------------------------------------------------------------------------------ + + # Check 'To consume line' with product c and uom kg. + # ------------------------------------------------- + + moves = self.StockMove.search([ + ('raw_material_production_id', '=', mnf_product_a.id), + ('product_id', '=', product_c.id), + ('product_uom', '=', self.uom_kg.id)]) + + # Check total consume line with product c and uom kg. + self.assertEqual(len(moves), 1, 'Production move lines are not generated proper.') + list_qty = {move.product_uom_qty for move in moves} + self.assertEqual(list_qty, {6.0}, "Wrong product quantity in 'To consume line' of manufacturing order.") + # Check state of consume line with product c and uom kg. + for move in moves: + self.assertEqual(move.state, 'confirmed', "Wrong state in 'To consume line' of manufacturing order.") + + # Check 'To consume line' with product c and uom gm. + # --------------------------------------------------- + + move = self.StockMove.search([ + ('raw_material_production_id', '=', mnf_product_a.id), + ('product_id', '=', product_c.id), + ('product_uom', '=', self.uom_gm.id)]) + + # Check total consume line of product c with gm. + self.assertEqual(len(move), 1, 'Production move lines are not generated proper.') + # Check quantity should be with 1502.5 ( 2 Dozen product A consume 300.5 gm then 10 Dozen (300.5 * (10/2)). + self.assertEqual(move.product_uom_qty, 1502.5, "Wrong product quantity in 'To consume line' of manufacturing order.") + # Check state of consume line with product c with and uom gm. + self.assertEqual(move.state, 'confirmed', "Wrong state in 'To consume line' of manufacturing order.") + + # Check 'To consume line' with product D. + # --------------------------------------- + + move = self.StockMove.search([ + ('raw_material_production_id', '=', mnf_product_a.id), + ('product_id', '=', product_d.id)]) + + # Check total consume line with product D. + self.assertEqual(len(move), 1, 'Production lines are not generated proper.') + + # <><><><><><><><><><><><><><><><><><><><><><> + # Manufacturing order for product D (20 unit). + # <><><><><><><><><><><><><><><><><><><><><><> + + # FP Todo: find a better way to look for the production order + mnf_product_d = self.MrpProduction.search([('product_id', '=', product_d.id)], order='id desc', limit=1) + # Check state of production order D. + self.assertEqual(mnf_product_d.state, 'confirmed', 'Manufacturing order should be confirmed.') + + # Check 'To consume line' state, quantity, uom of production order (product D). + # ----------------------------------------------------------------------------- + + move = self.StockMove.search([('raw_material_production_id', '=', mnf_product_d.id), ('product_id', '=', product_c.id)]) + self.assertEqual(move.product_uom_qty, 20, "Wrong product quantity in 'To consume line' of manufacturing order.") + self.assertEqual(move.product_uom.id, self.uom_kg.id, "Wrong unit of measure in 'To consume line' of manufacturing order.") + self.assertEqual(move.state, 'confirmed', "Wrong state in 'To consume line' of manufacturing order.") + + # ------------------------------- + # Create inventory for product c. + # ------------------------------- + # Need 20 kg product c to produce 20 unit product D. + # -------------------------------------------------- + + inventory = self.Inventory.create({ + 'name': 'Inventory Product KG', + 'product_ids': [(4, product_c.id)]}) + + inventory.action_start() + self.assertFalse(inventory.line_ids, "Inventory line should not created.") + self.InventoryLine.create({ + 'inventory_id': inventory.id, + 'product_id': product_c.id, + 'product_uom_id': self.uom_kg.id, + 'product_qty': 20, + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id}) + inventory.action_validate() + + # -------------------------------------------------- + # Assign product c to manufacturing order of product D. + # -------------------------------------------------- + + mnf_product_d.action_assign() + self.assertEqual(mnf_product_d.reservation_state, 'assigned', 'Availability should be assigned') + self.assertEqual(move.state, 'assigned', "Wrong state in 'To consume line' of manufacturing order.") + + # ------------------ + # produce product D. + # ------------------ + + mo_form = Form(mnf_product_d) + mo_form.qty_producing = 20 + mnf_product_d = mo_form.save() + mnf_product_d._post_inventory() + + # Check state of manufacturing order. + self.assertEqual(mnf_product_d.state, 'done', 'Manufacturing order should still be in progress state.') + # Check available quantity of product D. + self.assertEqual(product_d.qty_available, 20, 'Wrong quantity available of product D.') + + # ----------------------------------------------------------------- + # Check product D assigned or not to production order of product A. + # ----------------------------------------------------------------- + + self.assertEqual(mnf_product_a.state, 'confirmed', 'Manufacturing order should be confirmed.') + move = self.StockMove.search([('raw_material_production_id', '=', mnf_product_a.id), ('product_id', '=', product_d.id)]) + self.assertEqual(move.state, 'assigned', "Wrong state in 'To consume line' of manufacturing order.") + + # Create inventory for product C. + # ------------------------------ + # Need product C ( 20 kg + 6 kg + 1502.5 gm = 27.5025 kg) + # ------------------------------------------------------- + inventory = self.Inventory.create({ + 'name': 'Inventory Product C KG', + 'product_ids': [(4, product_c.id)]}) + + inventory.action_start() + self.assertFalse(inventory.line_ids, "Inventory line should not created.") + self.InventoryLine.create({ + 'inventory_id': inventory.id, + 'product_id': product_c.id, + 'product_uom_id': self.uom_kg.id, + 'product_qty': 27.5025, + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id}) + inventory.action_validate() + + # Assign product to manufacturing order of product A. + # --------------------------------------------------- + + mnf_product_a.action_assign() + self.assertEqual(mnf_product_a.reservation_state, 'assigned', 'Manufacturing order inventory state should be available.') + moves = self.StockMove.search([('raw_material_production_id', '=', mnf_product_a.id), ('product_id', '=', product_c.id)]) + + # Check product c move line state. + for move in moves: + self.assertEqual(move.state, 'assigned', "Wrong state in 'To consume line' of manufacturing order.") + + # Produce product A. + # ------------------ + + mo_form = Form(mnf_product_a) + mo_form.qty_producing = mo_form.product_qty + mnf_product_a = mo_form.save() + mnf_product_a._post_inventory() + # Check state of manufacturing order product A. + self.assertEqual(mnf_product_a.state, 'done', 'Manufacturing order should still be in the progress state.') + # Check product A avaialble quantity should be 120. + self.assertEqual(product_a.qty_available, 120, 'Wrong quantity available of product A.') + + def test_01_sale_mrp_delivery_kit(self): + """ Test delivered quantity on SO based on delivered quantity in pickings.""" + # intial so + product = self.env['product.product'].create({ + 'name': 'Table Kit', + 'type': 'consu', + 'invoice_policy': 'delivery', + 'categ_id': self.env.ref('product.product_category_all').id, + }) + # Remove the MTO route as purchase is not installed and since the procurement removal the exception is directly raised + product.write({'route_ids': [(6, 0, [self.company_data['default_warehouse'].manufacture_pull_id.route_id.id])]}) + + product_wood_panel = self.env['product.product'].create({ + 'name': 'Wood Panel', + 'type': 'product', + }) + product_desk_bolt = self.env['product.product'].create({ + 'name': 'Bolt', + 'type': 'product', + }) + self.env['mrp.bom'].create({ + 'product_tmpl_id': product.product_tmpl_id.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'sequence': 2, + 'type': 'phantom', + 'bom_line_ids': [ + (0, 0, { + 'product_id': product_wood_panel.id, + 'product_qty': 1, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + }), (0, 0, { + 'product_id': product_desk_bolt.id, + 'product_qty': 4, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + }) + ] + }) + + partner = self.env['res.partner'].create({'name': 'My Test Partner'}) + # if `delivery` module is installed, a default property is set for the carrier to use + # However this will lead to an extra line on the SO (the delivery line), which will force + # the SO to have a different flow (and `invoice_state` value) + if 'property_delivery_carrier_id' in partner: + partner.property_delivery_carrier_id = False + + f = Form(self.env['sale.order']) + f.partner_id = partner + with f.order_line.new() as line: + line.product_id = product + line.product_uom_qty = 5 + so = f.save() + + # confirm our standard so, check the picking + so.action_confirm() + self.assertTrue(so.picking_ids, 'Sale MRP: no picking created for "invoice on delivery" storable products') + + # invoice in on delivery, nothing should be invoiced + with self.assertRaises(UserError): + so._create_invoices() + self.assertEqual(so.invoice_status, 'no', 'Sale MRP: so invoice_status should be "nothing to invoice" after invoicing') + + # deliver partially (1 of each instead of 5), check the so's invoice_status and delivered quantities + pick = so.picking_ids + pick.move_lines.write({'quantity_done': 1}) + wiz_act = pick.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save() + wiz.process() + self.assertEqual(so.invoice_status, 'no', 'Sale MRP: so invoice_status should be "no" after partial delivery of a kit') + del_qty = sum(sol.qty_delivered for sol in so.order_line) + self.assertEqual(del_qty, 0.0, 'Sale MRP: delivered quantity should be zero after partial delivery of a kit') + # deliver remaining products, check the so's invoice_status and delivered quantities + self.assertEqual(len(so.picking_ids), 2, 'Sale MRP: number of pickings should be 2') + pick_2 = so.picking_ids.filtered('backorder_id') + for move in pick_2.move_lines: + if move.product_id.id == product_desk_bolt.id: + move.write({'quantity_done': 19}) + else: + move.write({'quantity_done': 4}) + pick_2.button_validate() + + del_qty = sum(sol.qty_delivered for sol in so.order_line) + self.assertEqual(del_qty, 5.0, 'Sale MRP: delivered quantity should be 5.0 after complete delivery of a kit') + self.assertEqual(so.invoice_status, 'to invoice', 'Sale MRP: so invoice_status should be "to invoice" after complete delivery of a kit') + + def test_02_sale_mrp_anglo_saxon(self): + """Test the price unit of a kit""" + # This test will check that the correct journal entries are created when a stockable product in real time valuation + # and in fifo cost method is sold in a company using anglo-saxon. + # For this test, let's consider a product category called Test category in real-time valuation and real price costing method + # Let's also consider a finished product with a bom with two components: component1(cost = 20) and component2(cost = 10) + # These products are in the Test category + # The bom consists of 2 component1 and 1 component2 + # The invoice policy of the finished product is based on delivered quantities + self.env.company.currency_id = self.env.ref('base.USD') + self.uom_unit = self.UoM.create({ + 'name': 'Test-Unit', + 'category_id': self.categ_unit.id, + 'factor': 1, + 'uom_type': 'bigger', + 'rounding': 1.0}) + self.company = self.company_data['company'] + self.company.anglo_saxon_accounting = True + self.partner = self.env['res.partner'].create({'name': 'My Test Partner'}) + self.category = self.env.ref('product.product_category_1').copy({'name': 'Test category','property_valuation': 'real_time', 'property_cost_method': 'fifo'}) + account_type = self.env['account.account.type'].create({'name': 'RCV type', 'type': 'other', 'internal_group': 'asset'}) + self.account_receiv = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00' , 'user_type_id': account_type.id, 'reconcile': True}) + account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00' , 'user_type_id': account_type.id, 'reconcile': True}) + account_output = self.env['account.account'].create({'name': 'Output', 'code': 'OUT00' , 'user_type_id': account_type.id, 'reconcile': True}) + account_valuation = self.env['account.account'].create({'name': 'Valuation', 'code': 'STV00' , 'user_type_id': account_type.id, 'reconcile': True}) + self.partner.property_account_receivable_id = self.account_receiv + self.category.property_account_income_categ_id = self.account_receiv + self.category.property_account_expense_categ_id = account_expense + self.category.property_stock_account_input_categ_id = self.account_receiv + self.category.property_stock_account_output_categ_id = account_output + self.category.property_stock_valuation_account_id = account_valuation + self.category.property_stock_journal = self.env['account.journal'].create({'name': 'Stock journal', 'type': 'sale', 'code': 'STK00'}) + + Product = self.env['product.product'] + self.finished_product = Product.create({ + 'name': 'Finished product', + 'type': 'product', + 'uom_id': self.uom_unit.id, + 'invoice_policy': 'delivery', + 'categ_id': self.category.id}) + self.component1 = Product.create({ + 'name': 'Component 1', + 'type': 'product', + 'uom_id': self.uom_unit.id, + 'categ_id': self.category.id, + 'standard_price': 20}) + self.component2 = Product.create({ + 'name': 'Component 2', + 'type': 'product', + 'uom_id': self.uom_unit.id, + 'categ_id': self.category.id, + 'standard_price': 10}) + + # Create quants with sudo to avoid: + # "You are not allowed to create 'Quants' (stock.quant) records. No group currently allows this operation." + self.env['stock.quant'].sudo().create({ + 'product_id': self.component1.id, + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, + 'quantity': 6.0, + }) + self.env['stock.quant'].sudo().create({ + 'product_id': self.component2.id, + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, + 'quantity': 3.0, + }) + self.bom = self.env['mrp.bom'].create({ + 'product_tmpl_id': self.finished_product.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + BomLine = self.env['mrp.bom.line'] + BomLine.create({ + 'product_id': self.component1.id, + 'product_qty': 2.0, + 'bom_id': self.bom.id}) + BomLine.create({ + 'product_id': self.component2.id, + 'product_qty': 1.0, + 'bom_id': self.bom.id}) + + # Create a SO for a specific partner for three units of the finished product + so_vals = { + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, { + 'name': self.finished_product.name, + 'product_id': self.finished_product.id, + 'product_uom_qty': 3, + 'product_uom': self.finished_product.uom_id.id, + 'price_unit': self.finished_product.list_price + })], + 'pricelist_id': self.env.ref('product.list0').id, + 'company_id': self.company.id, + } + self.so = self.env['sale.order'].create(so_vals) + # Validate the SO + self.so.action_confirm() + # Deliver the three finished products + pick = self.so.picking_ids + # To check the products on the picking + self.assertEqual(pick.move_lines.mapped('product_id'), self.component1 | self.component2) + wiz_act = pick.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save() + wiz.process() + # Create the invoice + self.so._create_invoices() + self.invoice = self.so.invoice_ids + # Changed the invoiced quantity of the finished product to 2 + move_form = Form(self.invoice) + with move_form.invoice_line_ids.edit(0) as line_form: + line_form.quantity = 2.0 + self.invoice = move_form.save() + self.invoice.action_post() + aml = self.invoice.line_ids + aml_expense = aml.filtered(lambda l: l.is_anglo_saxon_line and l.debit > 0) + aml_output = aml.filtered(lambda l: l.is_anglo_saxon_line and l.credit > 0) + # Check that the cost of Good Sold entries are equal to 2* (2 * 20 + 1 * 10) = 100 + self.assertEqual(aml_expense.debit, 100, "Cost of Good Sold entry missing or mismatching") + self.assertEqual(aml_output.credit, 100, "Cost of Good Sold entry missing or mismatching") + + def test_03_sale_mrp_simple_kit_qty_delivered(self): + """ Test that the quantities delivered are correct when + a simple kit is ordered with multiple backorders + """ + + # kit_1 structure: + # ================ + + # kit_1 ---|- component_a x2 + # |- component_b x1 + # |- component_c x3 + + # Updating the quantities in stock to prevent + # a 'Not enough inventory' warning message. + stock_location = self.company_data['default_warehouse'].lot_stock_id + self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 20) + self.env['stock.quant']._update_available_quantity(self.component_b, stock_location, 10) + self.env['stock.quant']._update_available_quantity(self.component_c, stock_location, 30) + + # Creation of a sale order for x10 kit_1 + partner = self.env['res.partner'].create({'name': 'My Test Partner'}) + f = Form(self.env['sale.order']) + f.partner_id = partner + with f.order_line.new() as line: + line.product_id = self.kit_1 + line.product_uom_qty = 10.0 + + # Confirming the SO to trigger the picking creation + so = f.save() + so.action_confirm() + + # Check picking creation + self.assertEqual(len(so.picking_ids), 1) + picking_original = so.picking_ids[0] + move_lines = picking_original.move_lines + + # Check if the correct amount of stock.moves are created + self.assertEqual(len(move_lines), 3) + + # Check if BoM is created and is for a 'Kit' + bom_from_k1 = self.env['mrp.bom']._bom_find(product=self.kit_1) + self.assertEqual(self.bom_kit_1.id, bom_from_k1.id) + self.assertEqual(bom_from_k1.type, 'phantom') + + # Check there's only 1 order line on the SO and it's for x10 'kit_1' + order_lines = so.order_line + self.assertEqual(len(order_lines), 1) + order_line = order_lines[0] + self.assertEqual(order_line.product_id.id, self.kit_1.id) + self.assertEqual(order_line.product_uom_qty, 10.0) + + # Check if correct qty is ordered for each component of the kit + expected_quantities = { + self.component_a: 20, + self.component_b: 10, + self.component_c: 30, + } + self._assert_quantities(move_lines, expected_quantities) + + # Process only x1 of the first component then create a backorder for the missing components + picking_original.move_lines.sorted()[0].write({'quantity_done': 1}) + + wiz_act = so.picking_ids[0].button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save().process() + + # Check that the backorder was created, no kit should be delivered at this point + self.assertEqual(len(so.picking_ids), 2) + backorder_1 = so.picking_ids - picking_original + self.assertEqual(backorder_1.backorder_id.id, picking_original.id) + self.assertEqual(order_line.qty_delivered, 0) + + # Process only x6 each componenent in the picking + # Then create a backorder for the missing components + backorder_1.move_lines.write({'quantity_done': 6}) + wiz_act = backorder_1.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save().process() + + # Check that a backorder is created + self.assertEqual(len(so.picking_ids), 3) + backorder_2 = so.picking_ids - picking_original - backorder_1 + self.assertEqual(backorder_2.backorder_id.id, backorder_1.id) + + # With x6 unit of each components, we can only make 2 kits. + # So only 2 kits should be delivered + self.assertEqual(order_line.qty_delivered, 2) + + # Process x3 more unit of each components : + # - Now only 3 kits should be delivered + # - A backorder will be created, the SO should have 3 picking_ids linked to it. + backorder_2.move_lines.write({'quantity_done': 3}) + + wiz_act = backorder_2.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save().process() + + self.assertEqual(len(so.picking_ids), 4) + backorder_3 = so.picking_ids - picking_original - backorder_2 - backorder_1 + self.assertEqual(backorder_3.backorder_id.id, backorder_2.id) + self.assertEqual(order_line.qty_delivered, 3) + + # Adding missing components + qty_to_process = { + self.component_a: 10, + self.component_b: 1, + self.component_c: 21, + } + self._process_quantities(backorder_3.move_lines, qty_to_process) + + # Validating the last backorder now it's complete + backorder_3.button_validate() + order_line._compute_qty_delivered() + + # All kits should be delivered + self.assertEqual(order_line.qty_delivered, 10) + + def test_04_sale_mrp_kit_qty_delivered(self): + """ Test that the quantities delivered are correct when + a kit with subkits is ordered with multiple backorders and returns + """ + + # 'kit_parent' structure: + # --------------------------- + # + # kit_parent --|- kit_2 x2 --|- component_d x1 + # | |- kit_1 x2 -------|- component_a x2 + # | |- component_b x1 + # | |- component_c x3 + # | + # |- kit_3 x1 --|- component_f x1 + # | |- component_g x2 + # | + # |- component_e x1 + + # Updating the quantities in stock to prevent + # a 'Not enough inventory' warning message. + stock_location = self.company_data['default_warehouse'].lot_stock_id + self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 56) + self.env['stock.quant']._update_available_quantity(self.component_b, stock_location, 28) + self.env['stock.quant']._update_available_quantity(self.component_c, stock_location, 84) + self.env['stock.quant']._update_available_quantity(self.component_d, stock_location, 14) + self.env['stock.quant']._update_available_quantity(self.component_e, stock_location, 7) + self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 14) + self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 28) + + # Creation of a sale order for x7 kit_parent + partner = self.env['res.partner'].create({'name': 'My Test Partner'}) + f = Form(self.env['sale.order']) + f.partner_id = partner + with f.order_line.new() as line: + line.product_id = self.kit_parent + line.product_uom_qty = 7.0 + + so = f.save() + so.action_confirm() + + # Check picking creation, its move lines should concern + # only components. Also checks that the quantities are corresponding + # to the SO + self.assertEqual(len(so.picking_ids), 1) + order_line = so.order_line[0] + picking_original = so.picking_ids[0] + move_lines = picking_original.move_lines + products = move_lines.mapped('product_id') + kits = [self.kit_parent, self.kit_3, self.kit_2, self.kit_1] + components = [self.component_a, self.component_b, self.component_c, self.component_d, self.component_e, self.component_f, self.component_g] + expected_quantities = { + self.component_a: 56.0, + self.component_b: 28.0, + self.component_c: 84.0, + self.component_d: 14.0, + self.component_e: 7.0, + self.component_f: 14.0, + self.component_g: 28.0 + } + + self.assertEqual(len(move_lines), 7) + self.assertTrue(not any(kit in products for kit in kits)) + self.assertTrue(all(component in products for component in components)) + self._assert_quantities(move_lines, expected_quantities) + + # Process only 7 units of each component + qty_to_process = 7 + move_lines.write({'quantity_done': qty_to_process}) + + # Create a backorder for the missing componenents + wiz_act = picking_original.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save().process() + + # Check that a backorded is created + self.assertEqual(len(so.picking_ids), 2) + backorder_1 = so.picking_ids - picking_original + self.assertEqual(backorder_1.backorder_id.id, picking_original.id) + + # Even if some components are delivered completely, + # no KitParent should be delivered + self.assertEqual(order_line.qty_delivered, 0) + + # Process just enough components to make 1 kit_parent + qty_to_process = { + self.component_a: 1, + self.component_c: 5, + } + self._process_quantities(backorder_1.move_lines, qty_to_process) + + # Create a backorder for the missing componenents + wiz_act = backorder_1.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save().process() + + # Only 1 kit_parent should be delivered at this point + self.assertEqual(order_line.qty_delivered, 1) + + # Check that the second backorder is created + self.assertEqual(len(so.picking_ids), 3) + backorder_2 = so.picking_ids - picking_original - backorder_1 + self.assertEqual(backorder_2.backorder_id.id, backorder_1.id) + + # Set the components quantities that backorder_2 should have + expected_quantities = { + self.component_a: 48, + self.component_b: 21, + self.component_c: 72, + self.component_d: 7, + self.component_f: 7, + self.component_g: 21 + } + + # Check that the computed quantities are matching the theorical ones. + # Since component_e was totally processed, this componenent shouldn't be + # present in backorder_2 + self.assertEqual(len(backorder_2.move_lines), 6) + move_comp_e = backorder_2.move_lines.filtered(lambda m: m.product_id.id == self.component_e.id) + self.assertFalse(move_comp_e) + self._assert_quantities(backorder_2.move_lines, expected_quantities) + + # Process enough components to make x3 kit_parents + qty_to_process = { + self.component_a: 16, + self.component_b: 5, + self.component_c: 24, + self.component_g: 5 + } + self._process_quantities(backorder_2.move_lines, qty_to_process) + + # Create a backorder for the missing componenents + wiz_act = backorder_2.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save().process() + + # Check that x3 kit_parents are indeed delivered + self.assertEqual(order_line.qty_delivered, 3) + + # Check that the third backorder is created + self.assertEqual(len(so.picking_ids), 4) + backorder_3 = so.picking_ids - (picking_original + backorder_1 + backorder_2) + self.assertEqual(backorder_3.backorder_id.id, backorder_2.id) + + # Check the components quantities that backorder_3 should have + expected_quantities = { + self.component_a: 32, + self.component_b: 16, + self.component_c: 48, + self.component_d: 7, + self.component_f: 7, + self.component_g: 16 + } + self._assert_quantities(backorder_3.move_lines, expected_quantities) + + # Process all missing components + self._process_quantities(backorder_3.move_lines, expected_quantities) + + # Validating the last backorder now it's complete. + # All kits should be delivered + backorder_3.button_validate() + self.assertEqual(order_line.qty_delivered, 7.0) + + # Return all components processed by backorder_3 + stock_return_picking_form = Form(self.env['stock.return.picking'] + .with_context(active_ids=backorder_3.ids, active_id=backorder_3.ids[0], + active_model='stock.picking')) + return_wiz = stock_return_picking_form.save() + for return_move in return_wiz.product_return_moves: + return_move.write({ + 'quantity': expected_quantities[return_move.product_id], + 'to_refund': True + }) + res = return_wiz.create_returns() + return_pick = self.env['stock.picking'].browse(res['res_id']) + + # Process all components and validate the picking + wiz_act = return_pick.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save() + wiz.process() + + # Now quantity delivered should be 3 again + self.assertEqual(order_line.qty_delivered, 3) + + stock_return_picking_form = Form(self.env['stock.return.picking'] + .with_context(active_ids=return_pick.ids, active_id=return_pick.ids[0], + active_model='stock.picking')) + return_wiz = stock_return_picking_form.save() + for move in return_wiz.product_return_moves: + move.quantity = expected_quantities[move.product_id] + res = return_wiz.create_returns() + return_of_return_pick = self.env['stock.picking'].browse(res['res_id']) + + # Process all components except one of each + for move in return_of_return_pick.move_lines: + move.write({ + 'quantity_done': expected_quantities[move.product_id] - 1, + 'to_refund': True + }) + + wiz_act = return_of_return_pick.button_validate() + Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save().process() + + # As one of each component is missing, only 6 kit_parents should be delivered + self.assertEqual(order_line.qty_delivered, 6) + + # Check that the 4th backorder is created. + self.assertEqual(len(so.picking_ids), 7) + backorder_4 = so.picking_ids - (picking_original + backorder_1 + backorder_2 + backorder_3 + return_of_return_pick + return_pick) + self.assertEqual(backorder_4.backorder_id.id, return_of_return_pick.id) + + # Check the components quantities that backorder_4 should have + for move in backorder_4.move_lines: + self.assertEqual(move.product_qty, 1) + + @mute_logger('odoo.tests.common.onchange') + def test_05_mrp_sale_kit_availability(self): + """ + Check that the 'Not enough inventory' warning message shows correct + informations when a kit is ordered + """ + + warehouse_1 = self.env['stock.warehouse'].create({ + 'name': 'Warehouse 1', + 'code': 'WH1' + }) + warehouse_2 = self.env['stock.warehouse'].create({ + 'name': 'Warehouse 2', + 'code': 'WH2' + }) + + # Those are all componenents needed to make kit_parents + components = [self.component_a, self.component_b, self.component_c, self.component_d, self.component_e, + self.component_f, self.component_g] + + # Set enough quantities to make 1 kit_uom_in_kit in WH1 + self.env['stock.quant']._update_available_quantity(self.component_a, warehouse_1.lot_stock_id, 8) + self.env['stock.quant']._update_available_quantity(self.component_b, warehouse_1.lot_stock_id, 4) + self.env['stock.quant']._update_available_quantity(self.component_c, warehouse_1.lot_stock_id, 12) + self.env['stock.quant']._update_available_quantity(self.component_d, warehouse_1.lot_stock_id, 2) + self.env['stock.quant']._update_available_quantity(self.component_e, warehouse_1.lot_stock_id, 1) + self.env['stock.quant']._update_available_quantity(self.component_f, warehouse_1.lot_stock_id, 2) + self.env['stock.quant']._update_available_quantity(self.component_g, warehouse_1.lot_stock_id, 4) + + # Set quantities on WH2, but not enough to make 1 kit_parent + self.env['stock.quant']._update_available_quantity(self.component_a, warehouse_2.lot_stock_id, 7) + self.env['stock.quant']._update_available_quantity(self.component_b, warehouse_2.lot_stock_id, 3) + self.env['stock.quant']._update_available_quantity(self.component_c, warehouse_2.lot_stock_id, 12) + self.env['stock.quant']._update_available_quantity(self.component_d, warehouse_2.lot_stock_id, 1) + self.env['stock.quant']._update_available_quantity(self.component_e, warehouse_2.lot_stock_id, 1) + self.env['stock.quant']._update_available_quantity(self.component_f, warehouse_2.lot_stock_id, 1) + self.env['stock.quant']._update_available_quantity(self.component_g, warehouse_2.lot_stock_id, 4) + + # Creation of a sale order for x7 kit_parent + qty_ordered = 7 + f = Form(self.env['sale.order']) + f.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + f.warehouse_id = warehouse_2 + with f.order_line.new() as line: + line.product_id = self.kit_parent + line.product_uom_qty = qty_ordered + so = f.save() + order_line = so.order_line[0] + + # Check that not enough enough quantities are available in the warehouse set in the SO + # but there are enough quantities in Warehouse 1 for 1 kit_parent + kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id) + + # Check that not enough enough quantities are available in the warehouse set in the SO + # but there are enough quantities in Warehouse 1 for 1 kit_parent + self.assertEqual(kit_parent_wh_order.virtual_available, 0) + kit_parent_wh_order.invalidate_cache() + kit_parent_wh1 = self.kit_parent.with_context(warehouse=warehouse_1.id) + self.assertEqual(kit_parent_wh1.virtual_available, 1) + + # Check there arn't enough quantities available for the sale order + self.assertTrue(float_compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) == -1) + + # We receive enoug of each component in Warehouse 2 to make 3 kit_parent + qty_to_process = { + self.component_a: (17, self.uom_unit), + self.component_b: (12, self.uom_unit), + self.component_c: (25, self.uom_unit), + self.component_d: (5, self.uom_unit), + self.component_e: (2, self.uom_unit), + self.component_f: (5, self.uom_unit), + self.component_g: (8, self.uom_unit), + } + self._create_move_quantities(qty_to_process, components, warehouse_2) + + # As 'Warehouse 2' is the warehouse linked to the SO, 3 kits should be available + # But the quantity available in Warehouse 1 should stay 1 + kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id) + self.assertEqual(kit_parent_wh_order.virtual_available, 3) + kit_parent_wh_order.invalidate_cache() + kit_parent_wh1 = self.kit_parent.with_context(warehouse=warehouse_1.id) + self.assertEqual(kit_parent_wh1.virtual_available, 1) + + # Check there arn't enough quantities available for the sale order + self.assertTrue(float_compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) == -1) + + # We receive enough of each component in Warehouse 2 to make 7 kit_parent + qty_to_process = { + self.component_a: (32, self.uom_unit), + self.component_b: (16, self.uom_unit), + self.component_c: (48, self.uom_unit), + self.component_d: (8, self.uom_unit), + self.component_e: (4, self.uom_unit), + self.component_f: (8, self.uom_unit), + self.component_g: (16, self.uom_unit), + } + self._create_move_quantities(qty_to_process, components, warehouse_2) + + # Enough quantities should be available, no warning message should be displayed + kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id) + self.assertEqual(kit_parent_wh_order.virtual_available, 7) + + def test_06_kit_qty_delivered_mixed_uom(self): + """ + Check that the quantities delivered are correct when a kit involves + multiple UoMs on its components + """ + # Create some components + component_uom_unit = self._create_product('Comp Unit', self.uom_unit) + component_uom_dozen = self._create_product('Comp Dozen', self.uom_dozen) + component_uom_kg = self._create_product('Comp Kg', self.uom_kg) + + # Create a kit 'kit_uom_1' : + # ----------------------- + # + # kit_uom_1 --|- component_uom_unit x2 Test-Dozen + # |- component_uom_dozen x1 Test-Dozen + # |- component_uom_kg x3 Test-G + + kit_uom_1 = self._create_product('Kit 1', self.uom_unit) + + bom_kit_uom_1 = self.env['mrp.bom'].create({ + 'product_tmpl_id': kit_uom_1.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine = self.env['mrp.bom.line'] + BomLine.create({ + 'product_id': component_uom_unit.id, + 'product_qty': 2.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_1.id}) + BomLine.create({ + 'product_id': component_uom_dozen.id, + 'product_qty': 1.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_1.id}) + BomLine.create({ + 'product_id': component_uom_kg.id, + 'product_qty': 3.0, + 'product_uom_id': self.uom_gm.id, + 'bom_id': bom_kit_uom_1.id}) + + # Updating the quantities in stock to prevent + # a 'Not enough inventory' warning message. + stock_location = self.company_data['default_warehouse'].lot_stock_id + self.env['stock.quant']._update_available_quantity(component_uom_unit, stock_location, 240) + self.env['stock.quant']._update_available_quantity(component_uom_dozen, stock_location, 10) + self.env['stock.quant']._update_available_quantity(component_uom_kg, stock_location, 0.03) + + # Creation of a sale order for x10 kit_1 + partner = self.env['res.partner'].create({'name': 'My Test Partner'}) + f = Form(self.env['sale.order']) + f.partner_id = partner + with f.order_line.new() as line: + line.product_id = kit_uom_1 + line.product_uom_qty = 10.0 + + so = f.save() + so.action_confirm() + + picking_original = so.picking_ids[0] + move_lines = picking_original.move_lines + order_line = so.order_line[0] + + # Check that the quantities on the picking are the one expected for each components + for ml in move_lines: + corr_bom_line = bom_kit_uom_1.bom_line_ids.filtered(lambda b: b.product_id.id == ml.product_id.id) + computed_qty = ml.product_uom._compute_quantity(ml.product_uom_qty, corr_bom_line.product_uom_id) + self.assertEqual(computed_qty, order_line.product_uom_qty * corr_bom_line.product_qty) + + # Processe enough componenents in the picking to make 2 kit_uom_1 + # Then create a backorder for the missing components + qty_to_process = { + component_uom_unit: 48, + component_uom_dozen: 3, + component_uom_kg: 0.006 + } + self._process_quantities(move_lines, qty_to_process) + res = move_lines.picking_id.button_validate() + Form(self.env[res['res_model']].with_context(res['context'])).save().process() + + # Check that a backorder is created + self.assertEqual(len(so.picking_ids), 2) + backorder_1 = so.picking_ids - picking_original + self.assertEqual(backorder_1.backorder_id.id, picking_original.id) + + # Only 2 kits should be delivered + self.assertEqual(order_line.qty_delivered, 2) + + # Adding missing components + qty_to_process = { + component_uom_unit: 192, + component_uom_dozen: 7, + component_uom_kg: 0.024 + } + self._process_quantities(backorder_1.move_lines, qty_to_process) + + # Validating the last backorder now it's complete + backorder_1.button_validate() + order_line._compute_qty_delivered() + # All kits should be delivered + self.assertEqual(order_line.qty_delivered, 10) + + @mute_logger('odoo.tests.common.onchange') + def test_07_kit_availability_mixed_uom(self): + """ + Check that the 'Not enough inventory' warning message displays correct + informations when a kit with multiple UoMs on its components is ordered + """ + + # Create some components + component_uom_unit = self._create_product('Comp Unit', self.uom_unit) + component_uom_dozen = self._create_product('Comp Dozen', self.uom_dozen) + component_uom_kg = self._create_product('Comp Kg', self.uom_kg) + component_uom_gm = self._create_product('Comp g', self.uom_gm) + components = [component_uom_unit, component_uom_dozen, component_uom_kg, component_uom_gm] + + # Create a kit 'kit_uom_in_kit' : + # ----------------------- + # kit_uom_in_kit --|- component_uom_gm x3 Test-KG + # |- kit_uom_1 x2 Test-Dozen --|- component_uom_unit x2 Test-Dozen + # |- component_uom_dozen x1 Test-Dozen + # |- component_uom_kg x5 Test-G + + kit_uom_1 = self._create_product('Sub Kit 1', self.uom_unit) + kit_uom_in_kit = self._create_product('Parent Kit', self.uom_unit) + + bom_kit_uom_1 = self.env['mrp.bom'].create({ + 'product_tmpl_id': kit_uom_1.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine = self.env['mrp.bom.line'] + BomLine.create({ + 'product_id': component_uom_unit.id, + 'product_qty': 2.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_1.id}) + BomLine.create({ + 'product_id': component_uom_dozen.id, + 'product_qty': 1.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_1.id}) + BomLine.create({ + 'product_id': component_uom_kg.id, + 'product_qty': 5.0, + 'product_uom_id': self.uom_gm.id, + 'bom_id': bom_kit_uom_1.id}) + + bom_kit_uom_in_kit = self.env['mrp.bom'].create({ + 'product_tmpl_id': kit_uom_in_kit.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine.create({ + 'product_id': component_uom_gm.id, + 'product_qty': 3.0, + 'product_uom_id': self.uom_kg.id, + 'bom_id': bom_kit_uom_in_kit.id}) + BomLine.create({ + 'product_id': kit_uom_1.id, + 'product_qty': 2.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_in_kit.id}) + + # Create a simple warehouse to receives some products + warehouse_1 = self.env['stock.warehouse'].create({ + 'name': 'Warehouse 1', + 'code': 'WH1' + }) + + # Set enough quantities to make 1 kit_uom_in_kit in WH1 + self.env['stock.quant']._update_available_quantity(component_uom_unit, warehouse_1.lot_stock_id, 576) + self.env['stock.quant']._update_available_quantity(component_uom_dozen, warehouse_1.lot_stock_id, 24) + self.env['stock.quant']._update_available_quantity(component_uom_kg, warehouse_1.lot_stock_id, 0.12) + self.env['stock.quant']._update_available_quantity(component_uom_gm, warehouse_1.lot_stock_id, 3000) + + # Creation of a sale order for x5 kit_uom_in_kit + qty_ordered = 5 + f = Form(self.env['sale.order']) + f.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + f.warehouse_id = warehouse_1 + with f.order_line.new() as line: + line.product_id = kit_uom_in_kit + line.product_uom_qty = qty_ordered + + so = f.save() + order_line = so.order_line[0] + + # Check that not enough enough quantities are available in the warehouse set in the SO + # but there are enough quantities in Warehouse 1 for 1 kit_parent + kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities() + virtual_available_wh_order = kit_uom_in_kit.virtual_available + self.assertEqual(virtual_available_wh_order, 1) + + # Check there arn't enough quantities available for the sale order + self.assertTrue(float_compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) == -1) + + # We receive enough of each component in Warehouse 1 to make 3 kit_uom_in_kit. + # Moves are created instead of only updating the quant quantities in order to trigger every compute fields. + qty_to_process = { + component_uom_unit: (1152, self.uom_unit), + component_uom_dozen: (48, self.uom_dozen), + component_uom_kg: (0.24, self.uom_kg), + component_uom_gm: (6000, self.uom_gm) + } + self._create_move_quantities(qty_to_process, components, warehouse_1) + + # Check there arn't enough quantities available for the sale order + self.assertTrue(float_compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) == -1) + kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities() + virtual_available_wh_order = kit_uom_in_kit.virtual_available + self.assertEqual(virtual_available_wh_order, 3) + + # We process enough quantities to have enough kit_uom_in_kit available for the sale order. + self._create_move_quantities(qty_to_process, components, warehouse_1) + + # We check that enough quantities were processed to sell 5 kit_uom_in_kit + kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities() + self.assertEqual(kit_uom_in_kit.virtual_available, 5) + + def test_10_sale_mrp_kits_routes(self): + + # Create a kit 'kit_1' : + # ----------------------- + # + # kit_1 --|- component_shelf1 x3 + # |- component_shelf2 x2 + + stock_location_components = self.env['stock.location'].create({ + 'name': 'Shelf 1', + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, + }) + stock_location_14 = self.env['stock.location'].create({ + 'name': 'Shelf 2', + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, + }) + + kit_1 = self._create_product('Kit1', self.uom_unit) + component_shelf1 = self._create_product('Comp Shelf1', self.uom_unit) + component_shelf2 = self._create_product('Comp Shelf2', self.uom_unit) + + with Form(self.env['mrp.bom']) as bom: + bom.product_tmpl_id = kit_1.product_tmpl_id + bom.product_qty = 1 + bom.product_uom_id = self.uom_unit + bom.type = 'phantom' + with bom.bom_line_ids.new() as line: + line.product_id = component_shelf1 + line.product_qty = 3 + line.product_uom_id = self.uom_unit + with bom.bom_line_ids.new() as line: + line.product_id = component_shelf2 + line.product_qty = 2 + line.product_uom_id = self.uom_unit + + # Creating 2 specific routes for each of the components of the kit + route_shelf1 = self.env['stock.location.route'].create({ + 'name': 'Shelf1 -> Customer', + 'product_selectable': True, + 'rule_ids': [(0, 0, { + 'name': 'Shelf1 -> Customer', + 'action': 'pull', + 'picking_type_id': self.company_data['default_warehouse'].in_type_id.id, + 'location_src_id': stock_location_components.id, + 'location_id': self.ref('stock.stock_location_customers'), + })], + }) + + route_shelf2 = self.env['stock.location.route'].create({ + 'name': 'Shelf2 -> Customer', + 'product_selectable': True, + 'rule_ids': [(0, 0, { + 'name': 'Shelf2 -> Customer', + 'action': 'pull', + 'picking_type_id': self.company_data['default_warehouse'].in_type_id.id, + 'location_src_id': stock_location_14.id, + 'location_id': self.ref('stock.stock_location_customers'), + })], + }) + + component_shelf1.write({ + 'route_ids': [(4, route_shelf1.id)]}) + component_shelf2.write({ + 'route_ids': [(4, route_shelf2.id)]}) + + # Set enough quantities to make 1 kit_uom_in_kit in WH1 + self.env['stock.quant']._update_available_quantity(component_shelf1, self.company_data['default_warehouse'].lot_stock_id, 15) + self.env['stock.quant']._update_available_quantity(component_shelf2, self.company_data['default_warehouse'].lot_stock_id, 10) + + # Creating a sale order for 5 kits and confirming it + order_form = Form(self.env['sale.order']) + order_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + with order_form.order_line.new() as line: + line.product_id = kit_1 + line.product_uom = self.uom_unit + line.product_uom_qty = 5 + order = order_form.save() + order.action_confirm() + + # Now we check that the routes of the components were applied, in order to make sure the routes set + # on the kit itself are ignored + self.assertEqual(len(order.picking_ids), 2) + self.assertEqual(len(order.picking_ids[0].move_lines), 1) + self.assertEqual(len(order.picking_ids[1].move_lines), 1) + moves = order.picking_ids.mapped('move_lines') + move_shelf1 = moves.filtered(lambda m: m.product_id == component_shelf1) + move_shelf2 = moves.filtered(lambda m: m.product_id == component_shelf2) + self.assertEqual(move_shelf1.location_id.id, stock_location_components.id) + self.assertEqual(move_shelf1.location_dest_id.id, self.ref('stock.stock_location_customers')) + self.assertEqual(move_shelf2.location_id.id, stock_location_14.id) + self.assertEqual(move_shelf2.location_dest_id.id, self.ref('stock.stock_location_customers')) + + def test_11_sale_mrp_explode_kits_uom_quantities(self): + + # Create a kit 'kit_1' : + # ----------------------- + # + # 2x Dozens kit_1 --|- component_unit x6 Units + # |- component_kg x7 Kg + + kit_1 = self._create_product('Kit1', self.uom_unit) + component_unit = self._create_product('Comp Unit', self.uom_unit) + component_kg = self._create_product('Comp Kg', self.uom_kg) + + with Form(self.env['mrp.bom']) as bom: + bom.product_tmpl_id = kit_1.product_tmpl_id + bom.product_qty = 2 + bom.product_uom_id = self.uom_dozen + bom.type = 'phantom' + with bom.bom_line_ids.new() as line: + line.product_id = component_unit + line.product_qty = 6 + line.product_uom_id = self.uom_unit + with bom.bom_line_ids.new() as line: + line.product_id = component_kg + line.product_qty = 7 + line.product_uom_id = self.uom_kg + + # Create a simple warehouse to receives some products + warehouse_1 = self.env['stock.warehouse'].create({ + 'name': 'Warehouse 1', + 'code': 'WH1' + }) + # Set enough quantities to make 1 Test-Dozen kit_uom_in_kit + self.env['stock.quant']._update_available_quantity(component_unit, warehouse_1.lot_stock_id, 12) + self.env['stock.quant']._update_available_quantity(component_kg, warehouse_1.lot_stock_id, 14) + + # Creating a sale order for 3 Units of kit_1 and confirming it + order_form = Form(self.env['sale.order']) + order_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + order_form.warehouse_id = warehouse_1 + with order_form.order_line.new() as line: + line.product_id = kit_1 + line.product_uom = self.uom_unit + line.product_uom_qty = 2 + order = order_form.save() + order.action_confirm() + + # Now we check that the routes of the components were applied, in order to make sure the routes set + # on the kit itself are ignored + self.assertEqual(len(order.picking_ids), 1) + self.assertEqual(len(order.picking_ids[0].move_lines), 2) + + # Finally, we check the quantities for each component on the picking + move_component_unit = order.picking_ids[0].move_lines.filtered(lambda m: m.product_id == component_unit) + move_component_kg = order.picking_ids[0].move_lines - move_component_unit + self.assertEqual(move_component_unit.product_uom_qty, 0.5) + self.assertEqual(move_component_kg.product_uom_qty, 0.58) + + def test_product_type_service_1(self): + route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id.id + route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id.id + self.uom_unit = self.env.ref('uom.product_uom_unit') + + # Create finished product + finished_product = self.env['product.product'].create({ + 'name': 'Geyser', + 'type': 'product', + 'route_ids': [(4, route_mto), (4, route_manufacture)], + }) + + # Create service type product + product_raw = self.env['product.product'].create({ + 'name': 'raw Geyser', + 'type': 'service', + }) + + # Create bom for finish product + bom = self.env['mrp.bom'].create({ + 'product_id': finished_product.id, + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})] + }) + + # Create sale order + sale_form = Form(self.env['sale.order']) + sale_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + with sale_form.order_line.new() as line: + line.name = finished_product.name + line.product_id = finished_product + line.product_uom_qty = 1.0 + line.product_uom = self.uom_unit + line.price_unit = 10.0 + sale_order = sale_form.save() + + sale_order.action_confirm() + + mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)]) + + self.assertTrue(mo, 'Manufacturing order created.') + + def test_cancel_flow_1(self): + """ Sell a MTO/manufacture product. + + Cancel the delivery and the production order. Then duplicate + the delivery. Another production order should be created.""" + route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id.id + route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id.id + self.uom_unit = self.env.ref('uom.product_uom_unit') + + # Create finished product + finished_product = self.env['product.product'].create({ + 'name': 'Geyser', + 'type': 'product', + 'route_ids': [(4, route_mto), (4, route_manufacture)], + }) + + product_raw = self.env['product.product'].create({ + 'name': 'raw Geyser', + 'type': 'product', + }) + + # Create bom for finish product + bom = self.env['mrp.bom'].create({ + 'product_id': finished_product.id, + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})] + }) + + # Create sale order + sale_form = Form(self.env['sale.order']) + sale_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + with sale_form.order_line.new() as line: + line.name = finished_product.name + line.product_id = finished_product + line.product_uom_qty = 1.0 + line.product_uom = self.uom_unit + line.price_unit = 10.0 + sale_order = sale_form.save() + + sale_order.action_confirm() + + mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)]) + delivery = sale_order.picking_ids + delivery.action_cancel() + mo.action_cancel() + copied_delivery = delivery.copy() + copied_delivery.action_confirm() + mos = self.env['mrp.production'].search([('product_id', '=', finished_product.id)]) + self.assertEqual(len(mos), 1) + self.assertEqual(mos.state, 'cancel') + + def test_cancel_flow_2(self): + """ Sell a MTO/manufacture product. + + Cancel the production order and the delivery. Then duplicate + the delivery. Another production order should be created.""" + route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id.id + route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id.id + self.uom_unit = self.env.ref('uom.product_uom_unit') + + # Create finished product + finished_product = self.env['product.product'].create({ + 'name': 'Geyser', + 'type': 'product', + 'route_ids': [(4, route_mto), (4, route_manufacture)], + }) + + product_raw = self.env['product.product'].create({ + 'name': 'raw Geyser', + 'type': 'product', + }) + + # Create bom for finish product + bom = self.env['mrp.bom'].create({ + 'product_id': finished_product.id, + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})] + }) + + # Create sale order + sale_form = Form(self.env['sale.order']) + sale_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'}) + with sale_form.order_line.new() as line: + line.name = finished_product.name + line.product_id = finished_product + line.product_uom_qty = 1.0 + line.product_uom = self.uom_unit + line.price_unit = 10.0 + sale_order = sale_form.save() + + sale_order.action_confirm() + + mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)]) + delivery = sale_order.picking_ids + mo.action_cancel() + delivery.action_cancel() + copied_delivery = delivery.copy() + copied_delivery.action_confirm() + mos = self.env['mrp.production'].search([('product_id', '=', finished_product.id)]) + self.assertEqual(len(mos), 1) + self.assertEqual(mos.state, 'cancel') diff --git a/addons/sale_mrp/tests/test_sale_mrp_kit_bom.py b/addons/sale_mrp/tests/test_sale_mrp_kit_bom.py new file mode 100644 index 00000000..4c1978a9 --- /dev/null +++ b/addons/sale_mrp/tests/test_sale_mrp_kit_bom.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase, Form + + +class TestSaleMrpKitBom(TransactionCase): + + def _create_product(self, name, type, price): + return self.env['product.product'].create({ + 'name': name, + 'type': type, + 'standard_price': price, + }) + + def test_sale_mrp_kit_bom_cogs(self): + """Check invoice COGS aml after selling and delivering a product + with Kit BoM having another product with Kit BoM as component""" + + # ---------------------------------------------- + # BoM of Kit A: + # - BoM Type: Kit + # - Quantity: 3 + # - Components: + # * 2 x Kit B + # * 1 x Component A (Cost: $3) + # + # BoM of Kit B: + # - BoM Type: Kit + # - Quantity: 10 + # - Components: + # * 2 x Component B (Cost: $4) + # * 3 x Component BB (Cost: $5) + # ---------------------------------------------- + + self.env.user.company_id.anglo_saxon_accounting = True + self.stock_input_account = self.env['account.account'].create({ + 'name': 'Stock Input', + 'code': 'StockIn', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + }) + self.stock_output_account = self.env['account.account'].create({ + 'name': 'Stock Output', + 'code': 'StockOut', + 'reconcile': True, + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + }) + self.stock_valuation_account = self.env['account.account'].create({ + 'name': 'Stock Valuation', + 'code': 'StockVal', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + }) + self.expense_account = self.env['account.account'].create({ + 'name': 'Expense Account', + 'code': 'Exp', + 'user_type_id': self.env.ref('account.data_account_type_expenses').id, + }) + self.income_account = self.env['account.account'].create({ + 'name': 'Income Account', + 'code': 'Inc', + 'user_type_id': self.env.ref('account.data_account_type_expenses').id, + }) + self.stock_journal = self.env['account.journal'].create({ + 'name': 'Stock Journal', + 'code': 'STJTEST', + 'type': 'general', + }) + self.recv_account = self.env['account.account'].create({ + 'name': 'account receivable', + 'code': 'RECV', + 'user_type_id': self.env.ref('account.data_account_type_receivable').id, + 'reconcile': True, + }) + self.pay_account = self.env['account.account'].create({ + 'name': 'account payable', + 'code': 'PAY', + 'user_type_id': self.env.ref('account.data_account_type_payable').id, + 'reconcile': True, + }) + self.customer = self.env['res.partner'].create({ + 'name': 'customer', + 'property_account_receivable_id': self.recv_account.id, + 'property_account_payable_id': self.pay_account.id, + }) + self.journal_sale = self.env['account.journal'].create({ + 'name': 'Sale Journal - Test', + 'code': 'AJ-SALE', + 'type': 'sale', + 'company_id': self.env.user.company_id.id, + }) + + self.component_a = self._create_product('Component A', 'consu', 3.00) + self.component_b = self._create_product('Component B', 'consu', 4.00) + self.component_bb = self._create_product('Component BB', 'consu', 5.00) + self.kit_a = self._create_product('Kit A', 'product', 0.00) + self.kit_b = self._create_product('Kit B', 'consu', 0.00) + + self.kit_a.write({ + 'categ_id': self.env.ref('product.product_category_all').id, + 'property_account_expense_id': self.expense_account.id, + 'property_account_income_id': self.income_account.id, + }) + self.kit_a.categ_id.write({ + 'property_stock_account_input_categ_id': self.stock_input_account.id, + 'property_stock_account_output_categ_id': self.stock_output_account.id, + 'property_stock_valuation_account_id': self.stock_valuation_account.id, + 'property_stock_journal': self.stock_journal.id, + 'property_valuation': 'real_time', + }) + + # Create BoM for Kit A + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.kit_a + bom_product_form.product_tmpl_id = self.kit_a.product_tmpl_id + bom_product_form.product_qty = 3.0 + bom_product_form.type = 'phantom' + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.kit_b + bom_line.product_qty = 2.0 + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.component_a + bom_line.product_qty = 1.0 + self.bom_a = bom_product_form.save() + + # Create BoM for Kit B + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.kit_b + bom_product_form.product_tmpl_id = self.kit_b.product_tmpl_id + bom_product_form.product_qty = 10.0 + bom_product_form.type = 'phantom' + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.component_b + bom_line.product_qty = 2.0 + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.component_bb + bom_line.product_qty = 3.0 + self.bom_b = bom_product_form.save() + + so = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'order_line': [ + (0, 0, { + 'name': self.kit_a.name, + 'product_id': self.kit_a.id, + 'product_uom_qty': 1.0, + 'product_uom': self.kit_a.uom_id.id, + 'price_unit': 1, + 'tax_id': False, + })], + }) + so.action_confirm() + so.picking_ids.move_lines.quantity_done = 1 + so.picking_ids.button_validate() + + invoice = so.with_context(default_journal_id=self.journal_sale.id)._create_invoices() + invoice.action_post() + + # Check the resulting accounting entries + amls = invoice.line_ids + self.assertEqual(len(amls), 4) + stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.stock_output_account) + self.assertEqual(stock_out_aml.debit, 0) + self.assertAlmostEqual(stock_out_aml.credit, 2.53) + cogs_aml = amls.filtered(lambda aml: aml.account_id == self.expense_account) + self.assertAlmostEqual(cogs_aml.debit, 2.53) + self.assertEqual(cogs_aml.credit, 0) diff --git a/addons/sale_mrp/tests/test_sale_mrp_lead_time.py b/addons/sale_mrp/tests/test_sale_mrp_lead_time.py new file mode 100644 index 00000000..e9d0e171 --- /dev/null +++ b/addons/sale_mrp/tests/test_sale_mrp_lead_time.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta + +from odoo import fields +from odoo.addons.stock.tests.common2 import TestStockCommon + +from odoo.tests import Form + + +class TestSaleMrpLeadTime(TestStockCommon): + + def setUp(self): + super(TestSaleMrpLeadTime, self).setUp() + self.env.ref('stock.route_warehouse0_mto').active = True + # Update the product_1 with type, route, Manufacturing Lead Time and Customer Lead Time + with Form(self.product_1) as p1: + p1.type = 'product' + p1.produce_delay = 5.0 + p1.sale_delay = 5.0 + p1.route_ids.clear() + p1.route_ids.add(self.warehouse_1.manufacture_pull_id.route_id) + p1.route_ids.add(self.warehouse_1.mto_pull_id.route_id) + + # Update the product_2 with type + with Form(self.product_2) as p2: + p2.type = 'consu' + + # Create Bill of materials for product_1 + with Form(self.env['mrp.bom']) as bom: + bom.product_tmpl_id = self.product_1.product_tmpl_id + bom.product_qty = 2 + with bom.bom_line_ids.new() as line: + line.product_id = self.product_2 + line.product_qty = 4 + + def test_00_product_company_level_delays(self): + """ In order to check schedule date, set product's Manufacturing Lead Time + and Customer Lead Time and also set company's Manufacturing Lead Time + and Sales Safety Days.""" + + company = self.env.ref('base.main_company') + + # Update company with Manufacturing Lead Time and Sales Safety Days + company.write({'manufacturing_lead': 3.0, + 'security_lead': 3.0}) + + # Create sale order of product_1 + order_form = Form(self.env['sale.order']) + order_form.partner_id = self.partner_1 + with order_form.order_line.new() as line: + line.product_id = self.product_1 + line.product_uom_qty = 10 + order = order_form.save() + # Confirm sale order + order.action_confirm() + + # Check manufacturing order created or not + manufacturing_order = self.env['mrp.production'].search([('product_id', '=', self.product_1.id), ('move_dest_ids', 'in', order.picking_ids[0].move_lines.ids)]) + self.assertTrue(manufacturing_order, 'Manufacturing order should be created.') + + # Check schedule date of picking + deadline_picking = fields.Datetime.from_string(order.date_order) + timedelta(days=self.product_1.sale_delay) + out_date = deadline_picking - timedelta(days=company.security_lead) + self.assertAlmostEqual( + order.picking_ids[0].scheduled_date, out_date, + delta=timedelta(seconds=1), + msg='Schedule date of picking should be equal to: Order date + Customer Lead Time - Sales Safety Days.' + ) + self.assertAlmostEqual( + order.picking_ids[0].date_deadline, deadline_picking, + delta=timedelta(seconds=1), + msg='Deadline date of picking should be equal to: Order date + Customer Lead Time.' + ) + + # Check schedule date and deadline of manufacturing order + mo_scheduled = out_date - timedelta(days=self.product_1.produce_delay) - timedelta(days=company.manufacturing_lead) + self.assertAlmostEqual( + fields.Datetime.from_string(manufacturing_order.date_planned_start), mo_scheduled, + delta=timedelta(seconds=1), + msg="Schedule date of manufacturing order should be equal to: Schedule date of picking - product's Manufacturing Lead Time - company's Manufacturing Lead Time." + ) + self.assertAlmostEqual( + fields.Datetime.from_string(manufacturing_order.date_deadline), deadline_picking, + delta=timedelta(seconds=1), + msg="Deadline date of manufacturing order should be equal to the deadline of sale picking" + ) + + def test_01_product_route_level_delays(self): + """ In order to check schedule dates, set product's Manufacturing Lead Time + and Customer Lead Time and also set warehouse route's delay.""" + + # Update warehouse_1 with Outgoing Shippings pick + pack + ship + self.warehouse_1.write({'delivery_steps': 'pick_pack_ship'}) + + # Set delay on pull rule + for pull_rule in self.warehouse_1.delivery_route_id.rule_ids: + pull_rule.write({'delay': 2}) + + # Create sale order of product_1 + order_form = Form(self.env['sale.order']) + order_form.partner_id = self.partner_1 + order_form.warehouse_id = self.warehouse_1 + with order_form.order_line.new() as line: + line.product_id = self.product_1 + line.product_uom_qty = 6 + order = order_form.save() + # Confirm sale order + order.action_confirm() + + # Run scheduler + self.env['procurement.group'].run_scheduler() + + # Check manufacturing order created or not + manufacturing_order = self.env['mrp.production'].search([('product_id', '=', self.product_1.id)]) + self.assertTrue(manufacturing_order, 'Manufacturing order should be created.') + + # Check the picking crated or not + self.assertTrue(order.picking_ids, "Pickings should be created.") + + # Check schedule date of ship type picking + out = order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse_1.out_type_id) + out_min_date = fields.Datetime.from_string(out.scheduled_date) + out_date = fields.Datetime.from_string(order.date_order) + timedelta(days=self.product_1.sale_delay) - timedelta(days=out.move_lines[0].rule_id.delay) + self.assertAlmostEqual( + out_min_date, out_date, + delta=timedelta(seconds=10), + msg='Schedule date of ship type picking should be equal to: order date + Customer Lead Time - pull rule delay.' + ) + + # Check schedule date of pack type picking + pack = order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse_1.pack_type_id) + pack_min_date = fields.Datetime.from_string(pack.scheduled_date) + pack_date = out_date - timedelta(days=pack.move_lines[0].rule_id.delay) + self.assertAlmostEqual( + pack_min_date, pack_date, + delta=timedelta(seconds=10), + msg='Schedule date of pack type picking should be equal to: Schedule date of ship type picking - pull rule delay.' + ) + + # Check schedule date of pick type picking + pick = order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse_1.pick_type_id) + pick_min_date = fields.Datetime.from_string(pick.scheduled_date) + self.assertAlmostEqual( + pick_min_date, pack_date, + delta=timedelta(seconds=10), + msg='Schedule date of pick type picking should be equal to: Schedule date of pack type picking.' + ) + + # Check schedule date and deadline date of manufacturing order + mo_scheduled = out_date - timedelta(days=self.product_1.produce_delay) - timedelta(days=self.warehouse_1.delivery_route_id.rule_ids[0].delay) - timedelta(days=self.env.ref('base.main_company').manufacturing_lead) + self.assertAlmostEqual( + fields.Datetime.from_string(manufacturing_order.date_planned_start), mo_scheduled, + delta=timedelta(seconds=1), + msg="Schedule date of manufacturing order should be equal to: Schedule date of picking - product's Manufacturing Lead Time- delay pull_rule." + ) + self.assertAlmostEqual( + manufacturing_order.date_deadline, order.picking_ids[0].date_deadline, + delta=timedelta(seconds=1), + msg="Deadline date of manufacturing order should be equal to the deadline of sale picking" + ) diff --git a/addons/sale_mrp/tests/test_sale_mrp_procurement.py b/addons/sale_mrp/tests/test_sale_mrp_procurement.py new file mode 100644 index 00000000..4de6c6d0 --- /dev/null +++ b/addons/sale_mrp/tests/test_sale_mrp_procurement.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import time + +from odoo.tests.common import TransactionCase, Form +from odoo.tools import mute_logger + + +class TestSaleMrpProcurement(TransactionCase): + + def test_sale_mrp(self): + self.env.ref('stock.route_warehouse0_mto').active = True + warehouse0 = self.env.ref('stock.warehouse0') + # In order to test the sale_mrp module in OpenERP, I start by creating a new product 'Slider Mobile' + # I define product category Mobile Products Sellable. + + with mute_logger('odoo.tests.common.onchange'): + # Suppress warning on "Changing your cost method" when creating a + # product category + pc = Form(self.env['product.category']) + pc.name = 'Mobile Products Sellable' + product_category_allproductssellable0 = pc.save() + + uom_unit = self.env.ref('uom.product_uom_unit') + + self.assertIn("seller_ids", self.env['product.template'].fields_get()) + + # I define product for Slider Mobile. + product = Form(self.env['product.template']) + + product.categ_id = product_category_allproductssellable0 + product.list_price = 200.0 + product.name = 'Slider Mobile' + product.type = 'product' + product.uom_id = uom_unit + product.uom_po_id = uom_unit + product.route_ids.clear() + product.route_ids.add(warehouse0.manufacture_pull_id.route_id) + product.route_ids.add(warehouse0.mto_pull_id.route_id) + product_template_slidermobile0 = product.save() + + product_template_slidermobile0.standard_price = 189 + + product_component = Form(self.env['product.product']) + product_component.name = 'Battery' + product_product_bettery = product_component.save() + + with Form(self.env['mrp.bom']) as bom: + bom.product_tmpl_id = product_template_slidermobile0 + with bom.bom_line_ids.new() as line: + line.product_id = product_product_bettery + line.product_qty = 4 + + # I create a sale order for product Slider mobile + so_form = Form(self.env['sale.order']) + so_form.partner_id = self.env['res.partner'].create({'name': 'Another Test Partner'}) + with so_form.order_line.new() as line: + line.product_id = product_template_slidermobile0.product_variant_ids + line.price_unit = 200 + line.product_uom_qty = 500.0 + line.customer_lead = 7.0 + sale_order_so0 = so_form.save() + + # I confirm the sale order + sale_order_so0.action_confirm() + + # I verify that a manufacturing order has been generated, and that its name and reference are correct + mo = self.env['mrp.production'].search([('origin', 'like', sale_order_so0.name)], limit=1) + self.assertTrue(mo, 'Manufacturing order has not been generated') + + def test_sale_mrp_pickings(self): + """ Test sale of multiple mrp products in MTO + to avoid generating multiple deliveries + to the customer location + """ + self.env.ref('stock.route_warehouse0_mto').active = True + # Create warehouse + self.customer_location = self.env['ir.model.data'].xmlid_to_res_id('stock.stock_location_customers') + self.warehouse = self.env['stock.warehouse'].create({ + 'name': 'Test Warehouse', + 'code': 'TWH' + }) + + self.uom_unit = self.env.ref('uom.product_uom_unit') + + # Create raw product for manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Raw Stick' + product_form.type = 'product' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + self.raw_product = product_form.save() + + # Create manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Stick' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + product_form.type = 'product' + product_form.route_ids.clear() + product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id) + product_form.route_ids.add(self.warehouse.mto_pull_id.route_id) + self.finished_product = product_form.save() + + # Create manifactured product which uses another manifactured + product_form = Form(self.env['product.product']) + product_form.name = 'Arrow' + product_form.type = 'product' + product_form.route_ids.clear() + product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id) + product_form.route_ids.add(self.warehouse.mto_pull_id.route_id) + self.complex_product = product_form.save() + + ## Create raw product for manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Raw Iron' + product_form.type = 'product' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + self.raw_product_2 = product_form.save() + + # Create bom for manufactured product + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.finished_product + bom_product_form.product_tmpl_id = self.finished_product.product_tmpl_id + bom_product_form.product_qty = 1.0 + bom_product_form.type = 'normal' + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.raw_product + bom_line.product_qty = 2.0 + + self.bom = bom_product_form.save() + + ## 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' + + so_form = Form(self.env['sale.order']) + so_form.partner_id = self.env['res.partner'].create({'name': 'Another Test Partner'}) + with so_form.order_line.new() as line: + line.product_id = self.complex_product + line.price_unit = 1 + line.product_uom_qty = 1 + with so_form.order_line.new() as line: + line.product_id = self.finished_product + line.price_unit = 1 + line.product_uom_qty = 1 + sale_order_so0 = so_form.save() + + sale_order_so0.action_confirm() + + pickings = sale_order_so0.picking_ids + + # One delivery... + self.assertEqual(len(pickings), 1) + + # ...with two products + move_lines = pickings[0].move_lines + self.assertEqual(len(move_lines), 2) |
