# Part of Odoo. See LICENSE file for full copyright and licensing details. from unittest import skip from odoo import Command from odoo.exceptions import UserError from odoo.tests import Form, common from odoo.tools import float_compare, mute_logger from odoo.addons.sale.tests.common import TestSaleCommon from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ( ValuationReconciliationTestCommon, ) # these tests create accounting entries, and therefore need a chart of accounts class TestSaleMrpFlowCommon(ValuationReconciliationTestCommon, TestSaleCommon): @classmethod def setUpClass(cls): super().setUpClass() # Required for `uom_id` to be visible in the view cls._enable_uom() 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.Quant = cls.env['stock.quant'] cls.ProductCategory = cls.env['product.category'] cls.uom_kg = cls.uom_kgm cls.uom_gm = cls.uom_gram cls.uom_ten = cls.UoM.create({ 'name': 'Test-Ten', 'relative_factor': 10, 'relative_uom_id': cls.uom_unit.id, }) # 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.is_storable = True p.uom_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': quantities_to_process[move.product_id], 'picked': True }) 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.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.quantity = qty_to_process[comp][0] move._action_done() @common.tagged('post_install', '-at_install') class TestSaleMrpFlow(TestSaleMrpFlowCommon): @skip('Temporary to fast merge new valuation') 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._cls_create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto]) product_c = self._cls_create_product('Product C', self.uom_kg) product_b = self._cls_create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto]) product_d = self._cls_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.0 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_id = self.uom_dozen line.product_uom_qty = 10 order = order_form.save() order.action_confirm() # Verify buttons are working as expected self.assertEqual(order.mrp_production_count, 2, "Mo for product A + child mo for product B") # =============================================================================== # 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 1500.0 gm. # [ # For 2 Dozen product A will consume 300.0 gm product C # then for 10 Dozen product A will consume 1500.0 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['stock.rule'].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, 10, 'Wrong product quantity in manufacturing order.') self.assertEqual(mnf_product_a.product_uom_id, self.uom_dozen, '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 1500.0 ( 2 Dozen product A consume 300.0 gm then 10 Dozen (300.0 * (10/2)). self.assertEqual(move.product_uom_qty, 1500.0, "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. # -------------------------------------------------- self.Quant.with_context(inventory_mode=True).create({ 'product_id': product_c.id, # uom = uom_kg 'inventory_quantity': 20, 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, }).action_apply_inventory() # -------------------------------------------------- # 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.button_mark_done() # 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 + 1500.0 gm = 27.500 kg) # ------------------------------------------------------- self.Quant.with_context(inventory_mode=True).create({ 'product_id': product_c.id, # uom = uom_kg 'inventory_quantity': 27.51, # round up due to kg.rounding = 0.01 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, }).action_apply_inventory() # 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.') @skip('Temporary to fast merge new valuation') 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', }) # 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', 'is_storable': True, }) product_desk_bolt = self.env['product.product'].create({ 'name': 'Bolt', 'is_storable': True, }) 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_ids.write({'quantity': 1, 'picked': True}) Form.from_action(self.env, pick.button_validate()).save().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_ids: if move.product_id.id == product_desk_bolt.id: move.write({'quantity': 19, 'picked': True}) else: move.write({'quantity': 4, 'picked': True}) 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') @skip('Temporary to fast merge new valuation') 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', 'relative_factor': 1, }) 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_goods').copy({ 'name': 'Test category', 'property_valuation': 'real_time', 'property_cost_method': 'fifo', }) self.account_receiv = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00', 'account_type': 'asset_receivable', 'reconcile': True}) account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00', 'account_type': 'liability_current', 'reconcile': True}) account_income = self.env['account.account'].create({'name': 'Income', 'code': 'INC00', 'account_type': 'asset_current', 'reconcile': True}) account_valuation = self.env['account.account'].create({'name': 'Valuation', 'code': 'STV00', 'account_type': 'asset_receivable', 'reconcile': True}) self.partner.property_account_receivable_id = self.account_receiv self.category.property_account_income_categ_id = account_income self.category.property_account_expense_categ_id = account_expense 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', 'is_storable': True, 'uom_id': self.uom_unit.id, 'invoice_policy': 'delivery', 'categ_id': self.category.id}) self.component1 = Product.create({ 'name': 'Component 1', 'is_storable': True, 'uom_id': self.uom_unit.id, 'categ_id': self.category.id, 'standard_price': 20}) self.component2 = Product.create({ 'name': 'Component 2', 'is_storable': True, '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, 'price_unit': self.finished_product.list_price })], '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_ids.mapped('product_id'), self.component1 | self.component2) pick.button_validate() # 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.display_type == 'cogs' and l.debit > 0) aml_output = aml.filtered(lambda l: l.display_type == 'cogs' 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_ids = picking_original.move_ids # Check if the correct amount of stock.moves are created self.assertEqual(len(move_ids), 3) # Check if BoM is created and is for a 'Kit' bom_from_k1 = self.env['mrp.bom']._bom_find(self.kit_1)[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_ids, expected_quantities) # Process only x1 of the first component then create a backorder for the missing components picking_original.move_ids.sorted()[0].write({'quantity': 1, 'picked': True}) Form.from_action(self.env, so.picking_ids[0].button_validate()).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_ids.write({'quantity': 6, 'picked': True}) Form.from_action(self.env, backorder_1.button_validate()).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_ids.write({'quantity': 3, 'picked': True}) Form.from_action(self.env, backorder_2.button_validate()).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_ids, 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_ids = picking_original.move_ids products = move_ids.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_ids), 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_ids, expected_quantities) # Process only 7 units of each component qty_to_process = 7 move_ids.write({'quantity': qty_to_process, 'picked': True}) # Create a backorder for the missing componenents Form.from_action(self.env, picking_original.button_validate()).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_ids, qty_to_process) # Create a backorder for the missing componenents Form.from_action(self.env, backorder_1.button_validate()).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_ids), 6) move_comp_e = backorder_2.move_ids.filtered(lambda m: m.product_id.id == self.component_e.id) self.assertFalse(move_comp_e) self._assert_quantities(backorder_2.move_ids, 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_ids, qty_to_process) # Create a backorder for the missing componenents Form.from_action(self.env, backorder_2.button_validate()).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_ids, expected_quantities) # Process all missing components self._process_quantities(backorder_3.move_ids, 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.action_create_returns() return_pick = self.env['stock.picking'].browse(res['res_id']) # Process all components and validate the picking return_pick.button_validate() # 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.action_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_ids: move.write({ 'quantity': expected_quantities[move.product_id] - 1, 'picked': True, 'to_refund': True }) Form.from_action(self.env, return_of_return_pick.button_validate()).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_ids: 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_id=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) self.env.invalidate_all() kit_parent_wh1 = self.kit_parent.with_context(warehouse_id=warehouse_1.id) self.assertEqual(kit_parent_wh1.virtual_available, 1) # Check there arn't enough quantities available for the sale order self.assertTrue(line.product_uom_id.compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0) == -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_id=so.warehouse_id.id) self.assertEqual(kit_parent_wh_order.virtual_available, 3) self.env.invalidate_all() kit_parent_wh1 = self.kit_parent.with_context(warehouse_id=warehouse_1.id) self.assertEqual(kit_parent_wh1.virtual_available, 1) # Check there arn't enough quantities available for the sale order self.assertTrue(line.product_uom_id.compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0) == -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_id=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._cls_create_product('Comp Unit', self.uom_unit) component_uom_dozen = self._cls_create_product('Comp Dozen', self.uom_dozen) component_uom_kg = self._cls_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._cls_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_ids = picking_original.move_ids order_line = so.order_line[0] # Check that the quantities on the picking are the one expected for each components for move in move_ids: corr_bom_line = bom_kit_uom_1.bom_line_ids.filtered(lambda b: b.product_id.id == move.product_id.id) computed_qty = move.product_uom._compute_quantity(move.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_ids, qty_to_process) Form.from_action(self.env, move_ids.picking_id.button_validate()).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_ids, 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._cls_create_product('Comp Unit', self.uom_unit) component_uom_dozen = self._cls_create_product('Comp Dozen', self.uom_dozen) component_uom_kg = self._cls_create_product('Comp Kg', self.uom_kg) component_uom_gm = self._cls_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._cls_create_product('Sub Kit 1', self.uom_unit) kit_uom_in_kit = self._cls_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_id=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(line.product_uom_id.compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0) == -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(line.product_uom_id.compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0) == -1) kit_uom_in_kit.with_context(warehouse_id=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_id=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_shelf_1 = self.env['stock.location'].create({ 'name': 'Shelf 1', 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, }) stock_shelf_2 = self.env['stock.location'].create({ 'name': 'Shelf 2', 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, }) kit_1 = self._cls_create_product('Kit1', self.uom_unit) component_shelf1 = self._cls_create_product('Comp Shelf1', self.uom_unit) component_shelf2 = self._cls_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.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'].out_type_id.id, 'location_src_id': stock_shelf_1.id, 'location_dest_id': self.ref('stock.stock_location_customers'), })], }) route_shelf2 = self.env['stock.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'].out_type_id.id, 'location_src_id': stock_shelf_2.id, 'location_dest_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_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_ids), 1) self.assertEqual(len(order.picking_ids[1].move_ids), 1) moves = order.picking_ids.move_ids 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_shelf_1.id) self.assertEqual(move_shelf1.location_dest_id.id, self.ref('stock.stock_location_customers')) self.assertEqual(move_shelf2.location_id.id, stock_shelf_2.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._cls_create_product('Kit1', self.uom_unit) component_unit = self._cls_create_product('Comp Unit', self.uom_unit) component_kg = self._cls_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_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_ids), 2) # Finally, we check the quantities for each component on the picking move_component_unit = order.picking_ids[0].move_ids.filtered(lambda m: m.product_id == component_unit) move_component_kg = order.picking_ids[0].move_ids - move_component_unit self.assertEqual(move_component_unit.product_uom_qty, 0.5) self.assertEqual(move_component_kg.product_uom_qty, 0.59) 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', 'is_storable': True, '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.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 route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id route_mto.rule_ids.procure_method = "make_to_order" self.uom_unit = self.env.ref('uom.product_uom_unit') # Create finished product finished_product = self.env['product.product'].create({ 'name': 'Geyser', 'is_storable': True, 'route_ids': [(4, route_mto.id), (4, route_manufacture.id)], }) product_raw = self.env['product.product'].create({ 'name': 'raw Geyser', 'is_storable': True, }) # 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.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 route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id route_mto.rule_ids.procure_method = "make_to_order" self.uom_unit = self.env.ref('uom.product_uom_unit') # Create finished product finished_product = self.env['product.product'].create({ 'name': 'Geyser', 'is_storable': True, 'route_ids': [(4, route_mto.id), (4, route_manufacture.id)], }) product_raw = self.env['product.product'].create({ 'name': 'raw Geyser', 'is_storable': True, }) # 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.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') def test_13_so_return_kit(self): """ Test that when returning a SO containing only a kit that contains another kit, the SO delivered quantities is set to 0 (with the all-or-nothing policy). Products : Main Kit Nested Kit Screw BoMs : Main Kit BoM (kit), recipe : Nested Kit Bom (kit), recipe : Screw Business flow : Create those Create a Sales order selling one Main Kit BoM Confirm the sales order Validate the delivery (outgoing) (qty_delivered = 1) Create a return for the delivery Validate return for delivery (ingoing) (qty_delivered = 0) """ main_kit_product = self.env['product.product'].create({ 'name': 'Main Kit', 'is_storable': True, }) nested_kit_product = self.env['product.product'].create({ 'name': 'Nested Kit', 'is_storable': True, }) product = self.env['product.product'].create({ 'name': 'Screw', 'is_storable': True, }) self.env['mrp.bom'].create({ 'product_id': nested_kit_product.id, 'product_tmpl_id': nested_kit_product.product_tmpl_id.id, 'product_qty': 1.0, 'type': 'phantom', 'bom_line_ids': [(5, 0), (0, 0, {'product_id': product.id})] }) self.env['mrp.bom'].create({ 'product_id': main_kit_product.id, 'product_tmpl_id': main_kit_product.product_tmpl_id.id, 'product_qty': 1.0, 'type': 'phantom', 'bom_line_ids': [(5, 0), (0, 0, {'product_id': nested_kit_product.id})] }) # Create a SO for product Main Kit Product order_form = Form(self.env['sale.order']) order_form.partner_id = self.env['res.partner'].create({'name': 'Test Partner'}) with order_form.order_line.new() as line: line.product_id = main_kit_product line.product_uom_qty = 1 order = order_form.save() order.action_confirm() qty_del_not_yet_validated = sum(sol.qty_delivered for sol in order.order_line) self.assertEqual(qty_del_not_yet_validated, 0.0, 'No delivery validated yet') # Validate delivery pick = order.picking_ids pick.move_ids.write({'quantity': 1, 'picked': True}) pick.button_validate() qty_del_validated = sum(sol.qty_delivered for sol in order.order_line) self.assertEqual(qty_del_validated, 1.0, 'The order went from warehouse to client, so it has been delivered') # 1 was delivered, now create a return stock_return_picking_form = Form(self.env['stock.return.picking'].with_context( active_ids=pick.ids, active_id=pick.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': 1, 'to_refund': True }) res = return_wiz.action_create_returns() return_pick = self.env['stock.picking'].browse(res['res_id']) return_pick.move_line_ids.quantity = 1 return_pick.button_validate() # validate return # Delivered quantities to the client should be 0 qty_del_return_validated = sum(sol.qty_delivered for sol in order.order_line) self.assertNotEqual(qty_del_return_validated, 1.0, "The return was validated, therefore the delivery from client to" " company was successful, and the client is left without his 1 product.") self.assertEqual(qty_del_return_validated, 0.0, "The return has processed, client doesn't have any quantity anymore") def test_14_change_bom_type(self): """ This test ensures that updating a Bom type during a flow does not lead to any error """ p1 = self._cls_create_product('Master', self.uom_unit) p2 = self._cls_create_product('Component', self.uom_unit) p3 = self.component_a p1.categ_id.write({ 'property_cost_method': 'average', 'property_valuation': 'real_time', }) stock_location = self.company_data['default_warehouse'].lot_stock_id self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 1) self.env['mrp.bom'].create({ 'product_tmpl_id': p1.product_tmpl_id.id, 'product_qty': 1.0, 'type': 'phantom', 'bom_line_ids': [(0, 0, { 'product_id': p2.id, 'product_qty': 1.0, })] }) p2_bom = self.env['mrp.bom'].create({ 'product_tmpl_id': p2.product_tmpl_id.id, 'product_qty': 1.0, 'type': 'phantom', 'bom_line_ids': [(0, 0, { 'product_id': p3.id, 'product_qty': 1.0, })] }) so_form = Form(self.env['sale.order']) so_form.partner_id = self.env['res.partner'].create({'name': 'Super Partner'}) with so_form.order_line.new() as so_line: so_line.product_id = p1 so = so_form.save() so.action_confirm() so.picking_ids.button_validate() p2_bom.type = "normal" so._create_invoices() invoice = so.invoice_ids invoice.action_post() self.assertEqual(invoice.state, 'posted') @skip('Temporary to fast merge new valuation') def test_15_anglo_saxon_variant_price_unit(self): """ Test the price unit of a variant from which template has another variant with kit bom. Products: Template A variant NOKIT variant KIT: Component A Business Flow: create products and kit create SO selling both variants validate the delivery create the invoice post the invoice """ # Create environment self.env.company.currency_id = self.env.ref('base.USD') self.env.company.anglo_saxon_accounting = True self.partner = self.env['res.partner'].create({'name': 'Test Partner'}) self.category = self.env.ref('product.product_category_goods').copy({ 'name': 'Test category', 'property_valuation': 'real_time', 'property_cost_method': 'fifo', }) self.stock_location = self.company_data['default_warehouse'].lot_stock_id # Create variant attributes self.prod_att_test = self.env['product.attribute'].create({'name': 'test'}) self.prod_attr_KIT = self.env['product.attribute.value'].create({'name': 'KIT', 'attribute_id': self.prod_att_test.id, 'sequence': 1}) self.prod_attr_NOKIT = self.env['product.attribute.value'].create({'name': 'NOKIT', 'attribute_id': self.prod_att_test.id, 'sequence': 2}) # Create the template self.product_template = self.env['product.template'].create({ 'name': 'Template A', 'is_storable': True, 'uom_id': self.uom_unit.id, 'invoice_policy': 'delivery', 'categ_id': self.category.id, 'attribute_line_ids': [(0, 0, { 'attribute_id': self.prod_att_test.id, 'value_ids': [(6, 0, [self.prod_attr_KIT.id, self.prod_attr_NOKIT.id])] })] }) # Create the variants self.pt_attr_KIT = self.product_template.attribute_line_ids[0].product_template_value_ids[0] self.pt_attr_NOKIT = self.product_template.attribute_line_ids[0].product_template_value_ids[1] self.variant_KIT = self.product_template._get_variant_for_combination(self.pt_attr_KIT) self.variant_NOKIT = self.product_template._get_variant_for_combination(self.pt_attr_NOKIT) # Assign a cost to the NOKIT variant self.variant_NOKIT.write({'standard_price': 25}) # Create the components self.comp_kit_a = self.env['product.product'].create({ 'name': 'Component Kit A', 'is_storable': True, 'uom_id': self.uom_unit.id, 'categ_id': self.category.id, 'standard_price': 20 }) self.comp_kit_b = self.env['product.product'].create({ 'name': 'Component Kit B', 'is_storable': True, 'uom_id': self.uom_unit.id, 'categ_id': self.category.id, 'standard_price': 10 }) # Create the bom bom = self.env['mrp.bom'].create({ 'product_tmpl_id': self.product_template.id, 'product_id': self.variant_KIT.id, 'product_qty': 1.0, 'type': 'phantom' }) self.env['mrp.bom.line'].create({ 'product_id': self.comp_kit_a.id, 'product_qty': 2.0, 'bom_id': bom.id }) self.env['mrp.bom.line'].create({ 'product_id': self.comp_kit_b.id, 'product_qty': 1.0, 'bom_id': bom.id }) # Create the quants self.env['stock.quant']._update_available_quantity(self.comp_kit_a, self.stock_location, 2) self.env['stock.quant']._update_available_quantity(self.comp_kit_b, self.stock_location, 1) self.env['stock.quant']._update_available_quantity(self.variant_NOKIT, self.stock_location, 1) # Create the sale order 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.variant_KIT.name, 'product_id': self.variant_KIT.id, 'product_uom_qty': 1, 'price_unit': 100, }), (0, 0, { 'name': self.variant_NOKIT.name, 'product_id': self.variant_NOKIT.id, 'product_uom_qty': 1, 'price_unit': 50 })], 'company_id': self.env.company.id } so = self.env['sale.order'].create(so_vals) # Validate the sale order so.action_confirm() # Deliver the products pick = so.picking_ids pick.button_validate() # Create the invoice so._create_invoices() # Validate the invoice invoice = so.invoice_ids invoice.action_post() amls = invoice.line_ids aml_kit_expense = amls.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0 and l.product_id == self.variant_KIT) aml_kit_output = amls.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0 and l.product_id == self.variant_KIT) aml_nokit_expense = amls.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0 and l.product_id == self.variant_NOKIT) aml_nokit_output = amls.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0 and l.product_id == self.variant_NOKIT) # Check that the Cost of Goods Sold for variant KIT is equal to 2*(2*20)+10 = 90 self.assertEqual(aml_kit_expense.debit, 90, "Cost of Good Sold entry missing or mismatching for variant with kit") self.assertEqual(aml_kit_output.credit, 90, "Cost of Good Sold entry missing or mismatching for variant with kit") # Check that the Cost of Goods Sold for variant NOKIT is equal to its standard_price = 25 self.assertEqual(aml_nokit_expense.debit, 25, "Cost of Good Sold entry missing or mismatching for variant without kit") self.assertEqual(aml_nokit_output.credit, 25, "Cost of Good Sold entry missing or mismatching for variant without kit") @skip('Temporary to fast merge new valuation') def test_16_anglo_saxon_variant_price_unit_multi_company(self): """ Test the price unit of the BOM of the stock move is taken Products: Template A variant KIT 1 variant KIT 2 Business Flow: create SO validate the delivery (cannot archive the BOM, because it is used in a non-invoiced line) update the BOM and create a new one create the invoice post the invoice """ # Create environment self.partner = self.env['res.partner'].create({'name': 'Test Partner'}) self.category = self.env.ref('product.product_category_goods').copy({ 'name': 'Test category', 'property_valuation': 'real_time', 'property_cost_method': 'fifo', }) account_receiv = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00', 'account_type': 'asset_receivable', 'reconcile': True}) account_income = self.env['account.account'].create({'name': 'Income', 'code': 'INC00', 'account_type': 'asset_current', 'reconcile': True}) account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00', 'account_type': 'liability_current', 'reconcile': True}) account_valuation = self.env['account.account'].create({'name': 'Valuation', 'code': 'STV00', 'account_type': 'asset_receivable', 'reconcile': True}) self.stock_location = self.company_data['default_warehouse'].lot_stock_id self.partner.property_account_receivable_id = account_receiv self.category.property_account_income_categ_id = account_income self.category.property_account_expense_categ_id = account_expense self.category.property_stock_account_input_categ_id = account_income self.category.property_stock_valuation_account_id = account_valuation # Create variant attributes self.prod_att_test = self.env['product.attribute'].create({'name': 'test'}) self.prod_attr_KIT_A = self.env['product.attribute.value'].create({'name': 'KIT A', 'attribute_id': self.prod_att_test.id, 'sequence': 1}) # Create the template self.product_template = self.env['product.template'].create({ 'name': 'Template A', 'is_storable': True, 'uom_id': self.uom_unit.id, 'invoice_policy': 'delivery', 'categ_id': self.category.id, 'attribute_line_ids': [(0, 0, { 'attribute_id': self.prod_att_test.id, 'value_ids': [(6, 0, [self.prod_attr_KIT_A.id])] })] }) # Create another variant self.pt_attr_KIT_A = self.product_template.attribute_line_ids[0].product_template_value_ids[0] self.variant_KIT_A = self.product_template._get_variant_for_combination(self.pt_attr_KIT_A) # Assign a cost to the NOKIT variant self.variant_KIT_A.write({'standard_price': 25}) # Create the components self.comp_kit_a = self.env['product.product'].create({ 'name': 'Component Kit A', 'is_storable': True, 'uom_id': self.uom_unit.id, 'categ_id': self.category.id, 'standard_price': 20 }) self.comp_kit_b = self.env['product.product'].create({ 'name': 'Component Kit B', 'is_storable': True, 'uom_id': self.uom_unit.id, 'categ_id': self.category.id, 'standard_price': 10 }) # Create the bom bom = self.env['mrp.bom'].create({ 'product_tmpl_id': self.product_template.id, 'product_id': self.variant_KIT_A.id, 'product_qty': 1.0, 'type': 'phantom', 'company_id': self.env.company.id, }) self.env['mrp.bom.line'].create({ 'product_id': self.comp_kit_a.id, 'product_qty': 1.0, 'company_id': self.env.company.id, 'bom_id': bom.id }) # Create the quants self.env['stock.quant']._update_available_quantity(self.comp_kit_a, self.stock_location, 2) self.env['stock.quant']._update_available_quantity(self.comp_kit_b, self.stock_location, 1) # Create the sale order 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.variant_KIT_A.name, 'product_id': self.variant_KIT_A.id, 'product_uom_qty': 1, 'price_unit': 50 })], 'company_id': self.env.company.id, } so = self.env['sale.order'].create(so_vals) # Validate the sale order so.action_confirm() # Deliver the products pick = so.picking_ids pick.button_validate() # Update BOM bom_updated = self.env['mrp.bom'].create({ 'product_tmpl_id': self.product_template.id, 'product_id': self.variant_KIT_A.id, 'product_qty': 1.0, 'type': 'phantom', 'company_id': self.env.company.id, }) self.env['mrp.bom.line'].create({ 'product_id': self.comp_kit_b.id, 'product_qty': 1.0, 'company_id': self.env.company.id, 'bom_id': bom_updated.id }) # Create the invoice so._create_invoices() # Validate the invoice invoice = so.invoice_ids invoice.action_post() amls = invoice.line_ids aml_nokit_expense = amls.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0 and l.product_id == self.variant_KIT_A) aml_nokit_output = amls.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0 and l.product_id == self.variant_KIT_A) # Check that the Cost of Goods Sold for variant NOKIT is equal to the cost of the first BOM self.assertEqual(aml_nokit_expense.debit, 20, "Cost of Good Sold entry missing or mismatching for variant without kit") self.assertEqual(aml_nokit_output.credit, 20, "Cost of Good Sold entry missing or mismatching for variant without kit") def test_reconfirm_cancelled_kit(self): so = self.env['sale.order'].create({ 'partner_id': self.env['res.partner'].create({'name': 'Test Partner'}).id, 'order_line': [ (0, 0, { 'name': self.kit_1.name, 'product_id': self.kit_1.id, 'product_uom_qty': 1.0, 'price_unit': 1.0, }) ], }) # 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, 10) 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, 10) so.action_confirm() # Check picking creation self.assertEqual(len(so.picking_ids), 1, "A picking should be created after the SO validation") so.picking_ids.button_validate() so._action_cancel() so.action_draft() so.action_confirm() self.assertEqual(len(so.picking_ids), 1, "The product was already delivered, no need to re-create a delivery order") @skip('Temporary to fast merge new valuation') def test_kit_margin_and_return_picking(self): """ This test ensure that, when returning the components of a sold kit, the sale order line cost does not change""" kit = self._cls_create_product('Super Kit', self.uom_unit) (kit + self.component_a).categ_id.property_cost_method = 'fifo' self.env['mrp.bom'].create({ 'product_tmpl_id': kit.product_tmpl_id.id, 'product_qty': 1.0, 'type': 'phantom', 'bom_line_ids': [(0, 0, { 'product_id': self.component_a.id, 'product_qty': 1.0, })] }) self.component_a.standard_price = 10 kit.button_bom_cost() stock_location = self.company_data['default_warehouse'].lot_stock_id self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 1) so_form = Form(self.env['sale.order']) so_form.partner_id = self.partner_a with so_form.order_line.new() as line: line.product_id = kit so = so_form.save() so.action_confirm() line = so.order_line price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids) self.assertEqual(price, 10) picking = so.picking_ids picking.button_validate() ctx = {'active_ids':picking.ids, 'active_id': picking.ids[0], 'active_model': 'stock.picking'} return_picking_wizard_form = Form(self.env['stock.return.picking'].with_context(ctx)) return_picking_wizard = return_picking_wizard_form.save() return_picking_wizard.product_return_moves.quantity = 1 return_picking_wizard.action_create_returns() price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids) self.assertEqual(price, 10) def test_kit_decrease_sol_qty(self): """ Create and confirm a SO with a qty. Increasing/Decreasing the SOL qty should update the qty on the delivery. Then, process the delivery, make a return and adapt the SOL qty -> there should not be any new picking """ stock_location = self.company_data['default_warehouse'].lot_stock_id custo_location = self.env.ref('stock.stock_location_customers') grp_uom = self.env.ref('uom.group_uom') self.env.user.write({'group_ids': [(4, grp_uom.id)]}) # 100 kit_3 = 100 x compo_f + 200 x compo_g self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 100) self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 200) so_form = Form(self.env['sale.order']) so_form.partner_id = self.partner_a with so_form.order_line.new() as line: line.product_id = self.kit_3 line.product_uom_qty = 7 line.product_uom_id = self.uom_ten so = so_form.save() so.action_confirm() delivery = so.picking_ids self.assertRecordValues(delivery.move_ids, [ {'product_id': self.component_f.id, 'product_uom_qty': 70}, {'product_id': self.component_g.id, 'product_uom_qty': 140}, ]) # Decrease with Form(so) as so_form: with so_form.order_line.edit(0) as line: line.product_uom_qty = 6 self.assertRecordValues(delivery.move_ids, [ {'product_id': self.component_f.id, 'product_uom_qty': 60}, {'product_id': self.component_g.id, 'product_uom_qty': 120}, ]) # Increase with Form(so) as so_form: with so_form.order_line.edit(0) as line: line.product_uom_qty = 10 self.assertRecordValues(delivery.move_ids, [ {'product_id': self.component_f.id, 'product_uom_qty': 100}, {'product_id': self.component_g.id, 'product_uom_qty': 200}, ]) delivery.button_validate() # Return 2 [uom_ten] x kit_3 return_wizard_form = Form(self.env['stock.return.picking'].with_context(active_ids=delivery.ids, active_id=delivery.id, active_model='stock.picking')) return_wizard = return_wizard_form.save() return_wizard.product_return_moves[0].quantity = 20 return_wizard.product_return_moves[1].quantity = 40 action = return_wizard.action_create_returns() return_picking = self.env['stock.picking'].browse(action['res_id']) return_picking.move_ids.picked = True return_picking.button_validate() # Adapt the SOL qty according to the delivered one with Form(so) as so_form: with so_form.order_line.edit(0) as line: line.product_uom_qty = 8 self.assertRecordValues(so.picking_ids.sorted('id').move_ids, [ {'product_id': self.component_f.id, 'location_dest_id': custo_location.id, 'quantity': 100, 'state': 'done'}, {'product_id': self.component_g.id, 'location_dest_id': custo_location.id, 'quantity': 200, 'state': 'done'}, {'product_id': self.component_f.id, 'location_dest_id': stock_location.id, 'quantity': 20, 'state': 'done'}, {'product_id': self.component_g.id, 'location_dest_id': stock_location.id, 'quantity': 40, 'state': 'done'}, ]) def test_kit_decrease_sol_qty_to_zero(self): """ Create and confirm a SO with a kit product. Increasing/Decreasing the SOL qty should update the qty on the delivery. """ stock_location = self.company_data['default_warehouse'].lot_stock_id grp_uom = self.env.ref('uom.group_uom') self.env.user.write({'group_ids': [(4, grp_uom.id)]}) # 10 kit_3 = 10 x compo_f + 20 x compo_g self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 10) self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 20) so_form = Form(self.env['sale.order']) so_form.partner_id = self.partner_a with so_form.order_line.new() as line: line.product_id = self.kit_3 line.product_uom_qty = 2 line.product_uom_id = self.uom_ten so = so_form.save() so.action_confirm() delivery = so.picking_ids self.assertRecordValues(delivery.move_ids, [ {'product_id': self.component_f.id, 'product_uom_qty': 20}, {'product_id': self.component_g.id, 'product_uom_qty': 40}, ]) # Decrease the qty to 0 with Form(so) as so_form: with so_form.order_line.edit(0) as line: line.product_uom_qty = 0 self.assertRecordValues(delivery.move_ids, [ {'product_id': self.component_f.id, 'product_uom_qty': 0}, {'product_id': self.component_g.id, 'product_uom_qty': 0}, ]) def test_kit_return_and_decrease_sol_qty_to_zero(self): """ Create and confirm a SO with a kit product. Deliver in two steps & Return the components Set the SOL qty to 0 Check that the move chain is adapted accordingly. """ stock_location = self.company_data['default_warehouse'].lot_stock_id self.company_data['default_warehouse'].delivery_steps = 'pick_ship' grp_uom = self.env.ref('uom.group_uom') self.env.user.write({'group_ids': [(4, grp_uom.id)]}) # 10 kit_3 = 10 x compo_f + 20 x compo_g self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 10) self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 20) so_form = Form(self.env['sale.order']) so_form.partner_id = self.partner_a with so_form.order_line.new() as line: line.product_id = self.kit_3 line.product_uom_qty = 2 line.product_uom_id = self.uom_ten so = so_form.save() so.action_confirm() pick = so.picking_ids for m in pick.move_ids: m.write({'quantity': m.product_uom_qty, 'picked': True}) pick.button_validate() self.assertEqual(pick.state, 'done') delivery = so.picking_ids - pick for m in delivery.move_ids: m.write({'quantity': m.product_uom_qty, 'picked': True}) delivery.button_validate() self.assertEqual(delivery.state, 'done') self.assertEqual(so.order_line.qty_delivered, 2) ctx = {'active_id': delivery.id, 'active_model': 'stock.picking'} return_wizard = Form(self.env['stock.return.picking'].with_context(ctx)).save() for line in return_wizard.product_return_moves: line.quantity = line.move_id.quantity return_picking = return_wizard._create_return() for m in return_picking.move_ids: m.write({'quantity': m.product_uom_qty, 'picked': True}) return_picking.button_validate() self.assertEqual(return_picking.state, 'done') self.assertEqual(so.order_line.qty_delivered, 0) with Form(so) as so_form: with so_form.order_line.edit(0) as line: line.product_uom_qty = 0 self.assertEqual(so.picking_ids, pick | delivery | return_picking) self.assertRecordValues(so.picking_ids.move_ids.sorted(lambda m: (m.picking_id.id, m.product_id.id)), [ {'picking_id': pick.id, 'product_id': self.component_f.id, 'quantity': 20.0}, {'picking_id': pick.id, 'product_id': self.component_g.id, 'quantity': 40.0}, {'picking_id': delivery.id, 'product_id': self.component_f.id, 'quantity': 20.0}, {'picking_id': delivery.id, 'product_id': self.component_g.id, 'quantity': 40.0}, {'picking_id': return_picking.id, 'product_id': self.component_f.id, 'quantity': 20.0}, {'picking_id': return_picking.id, 'product_id': self.component_g.id, 'quantity': 40.0}, ]) @skip('Temporary to fast merge new valuation') def test_fifo_reverse_and_create_new_invoice(self): """ FIFO automated Kit with one component Receive the component: 1@10, 1@50 Deliver 1 kit Post the invoice, add a credit note with option 'new draft inv' Post the second invoice COGS should be based on the delivered kit """ kit = self._cls_create_product('Simple Kit', self.uom_unit) categ_form = Form(self.env['product.category']) categ_form.name = 'Super Fifo' categ_form.property_cost_method = 'fifo' categ_form.property_valuation = 'real_time' categ = categ_form.save() (kit + self.component_a).categ_id = categ self.env['mrp.bom'].create({ 'product_tmpl_id': kit.product_tmpl_id.id, 'product_qty': 1.0, 'type': 'phantom', 'bom_line_ids': [(0, 0, {'product_id': self.component_a.id, 'product_qty': 1.0})] }) in_moves = self.env['stock.move'].create([{ 'product_id': self.component_a.id, 'location_id': self.env.ref('stock.stock_location_suppliers').id, 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id, 'product_uom': self.component_a.uom_id.id, 'product_uom_qty': 1, 'price_unit': p, } for p in [10, 50]]) in_moves._action_confirm() in_moves.write({'quantity': 1, 'picked': True}) in_moves._action_done() so = self.env['sale.order'].create({ 'partner_id': self.env['res.partner'].create({'name': 'Test Partner'}).id, 'order_line': [ (0, 0, { 'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1.0, 'price_unit': 100, 'tax_ids': False, })], }) so.action_confirm() picking = so.picking_ids picking.move_ids.write({'quantity': 1.0, 'picked': True}) picking.button_validate() invoice01 = so._create_invoices() invoice01.action_post() move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({ 'journal_id': invoice01.journal_id.id, }) reversal = move_reversal.modify_moves() invoice02 = self.env['account.move'].browse(reversal['res_id']) invoice02.action_post() amls = invoice02.line_ids stock_out_aml = amls.filtered(lambda aml: aml.account_id == categ.property_stock_account_output_categ_id) self.assertEqual(stock_out_aml.debit, 0) self.assertEqual(stock_out_aml.credit, 10) cogs_aml = amls.filtered(lambda aml: aml.account_id == categ.property_account_expense_categ_id) self.assertEqual(cogs_aml.debit, 10) self.assertEqual(cogs_aml.credit, 0) @skip('Temporary to fast merge new valuation') def test_kit_avco_amls_reconciliation(self): self.stock_account_product_categ.property_cost_method = 'average' compo01, compo02, kit = self.env['product.product'].create([{ 'name': name, 'is_storable': True, 'standard_price': price, 'categ_id': self.stock_account_product_categ.id, 'invoice_policy': 'delivery', } for name, price in [ ('Compo 01', 10), ('Compo 02', 20), ('Kit', 0), ]]) self.env['stock.quant']._update_available_quantity(compo01, self.company_data['default_warehouse'].lot_stock_id, 1) self.env['stock.quant']._update_available_quantity(compo02, self.company_data['default_warehouse'].lot_stock_id, 1) self.env['mrp.bom'].create({ 'product_id': kit.id, 'product_tmpl_id': kit.product_tmpl_id.id, 'product_uom_id': kit.uom_id.id, 'product_qty': 1.0, 'type': 'phantom', 'bom_line_ids': [ (0, 0, {'product_id': compo01.id, 'product_qty': 1.0}), (0, 0, {'product_id': compo02.id, 'product_qty': 1.0}), ], }) so = self.env['sale.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [ (0, 0, { 'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1.0, 'price_unit': 5, 'tax_ids': False, })], }) so.action_confirm() so.picking_ids.move_line_ids.quantity = 1 so.picking_ids.move_ids.picked = True so.picking_ids.button_validate() invoice = so._create_invoices() invoice.action_post() self.assertEqual(len(invoice.line_ids.filtered('reconciled')), 1) def test_avoid_removing_kit_bom_in_use(self): so = self.env['sale.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [ (0, 0, { 'name': self.kit_1.name, 'product_id': self.kit_1.id, 'product_uom_qty': 1.0, 'price_unit': 5, 'tax_ids': False, })], }) self.bom_kit_1.action_archive() self.bom_kit_1.action_unarchive() so.action_confirm() with self.assertRaises(UserError): self.bom_kit_1.write({'type': 'normal'}) with self.assertRaises(UserError): self.bom_kit_1.action_archive() with self.assertRaises(UserError): self.bom_kit_1.unlink() for move in so.order_line.move_ids: move.write({'quantity': move.product_uom_qty, 'picked': True}) so.picking_ids.button_validate() self.assertEqual(so.picking_ids.state, 'done') with self.assertRaises(UserError): self.bom_kit_1.write({'type': 'normal'}) with self.assertRaises(UserError): self.bom_kit_1.action_archive() with self.assertRaises(UserError): self.bom_kit_1.unlink() invoice = so._create_invoices() invoice.action_post() self.assertEqual(invoice.state, 'posted') self.bom_kit_1.action_archive() self.bom_kit_1.action_unarchive() self.bom_kit_1.write({'type': 'normal'}) self.bom_kit_1.write({'type': 'phantom'}) self.bom_kit_1.unlink() def test_merge_move_kit_on_adding_new_sol(self): """ Create and confirm an SO for 2 similar kit products. Add a new sale order line for an other unrelated prodcut. Check that the delivery kit moves were not merged by the confirmation of the new move. """ warehouse = self.company_data['default_warehouse'] warehouse.delivery_steps = 'pick_ship' kit = self.kit_3 # create a similar kit bom_copy = kit.bom_ids[0].copy() kit_copy = kit.copy() bom_copy.product_tmpl_id = kit_copy.product_tmpl_id # put component in stock: 10 kit = 10 x comp_f + 20 x comp_g self.env['stock.quant']._update_available_quantity(self.component_f, warehouse.lot_stock_id, 10) self.env['stock.quant']._update_available_quantity(self.component_g, warehouse.lot_stock_id, 20) self.env['stock.quant']._update_available_quantity(self.component_a, warehouse.lot_stock_id, 5) so_form = Form(self.env['sale.order']) so_form.partner_id = self.partner_a with so_form.order_line.new() as line: line.product_id = kit line.product_uom_qty = 2 with so_form.order_line.new() as line: line.product_id = kit_copy line.product_uom_qty = 3 so = so_form.save() so.action_confirm() pick = so.picking_ids.filtered(lambda p: p.picking_type_id == warehouse.pick_type_id) expected_pick_moves = [ { 'quantity': 2.0, 'product_id': self.component_f.id, 'bom_line_id': kit.bom_ids[0].bom_line_ids.filtered(lambda bl: bl.product_id == self.component_f).id}, { 'quantity': 3.0, 'product_id': self.component_f.id, 'bom_line_id': bom_copy.bom_line_ids.filtered(lambda bl: bl.product_id == self.component_f).id}, { 'quantity': 4.0, 'product_id': self.component_g.id, 'bom_line_id': kit.bom_ids[0].bom_line_ids.filtered(lambda bl: bl.product_id == self.component_g).id}, { 'quantity': 6.0, 'product_id': self.component_g.id, 'bom_line_id': bom_copy.bom_line_ids.filtered(lambda bl: bl.product_id == self.component_g).id}, ] self.assertRecordValues(pick.move_ids.sorted(lambda m: m.quantity), expected_pick_moves) with Form(so) as so_form: with so_form.order_line.new() as line: line.product_id = self.component_a line.product_uom_qty = 1 expected_pick_moves = [ { 'quantity': 1.0, 'product_id': self.component_a.id, 'bom_line_id': False}, ] + expected_pick_moves self.assertRecordValues(pick.move_ids.sorted(lambda m: m.quantity), expected_pick_moves) def test_return_kit_in_quarantine_location(self): """ Return a kit to WH/Return Location Push Rule: WH/Return -> WH/Stock Ensure the delivered qty is correctly updated """ wh = self.company_data['default_warehouse'] stock_location = wh.lot_stock_id return_location = self.env['stock.location'].create({ 'location_id': stock_location.location_id.id, 'name': 'Return Location', 'usage': 'internal', }) self.env['stock.route'].create({ 'name': 'Return Route', 'warehouse_selectable': True, 'warehouse_ids': [(4, wh.id)], 'rule_ids': [(0, 0, { 'name': 'Return to Stock', 'location_src_id': return_location.id, 'location_dest_id': stock_location.id, 'company_id': self.company_data['company'].id, 'action': 'push', 'auto': 'manual', 'picking_type_id': wh.int_type_id.id, })], }) order = self.env['sale.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [ (0, 0, {'product_id': self.kit_1.id}), ], }) order.action_confirm() delivery = order.picking_ids for move in delivery.move_ids: move.quantity = move.product_qty delivery.button_validate() self.assertEqual(delivery.state, 'done') return_wizard = self.env['stock.return.picking'].with_context(active_id=delivery.id, active_model='stock.picking').create({}) for line in return_wizard.product_return_moves: line.quantity = line.move_quantity res = return_wizard.action_create_returns() return_picking = self.env['stock.picking'].browse(res["res_id"]) return_picking.location_dest_id = return_location for move in return_picking.move_ids: move.quantity = move.product_qty return_picking.button_validate() self.assertEqual(return_picking.state, 'done') self.assertEqual(order.order_line.qty_delivered, 0) internal_picking = return_picking.move_ids.move_dest_ids.picking_id self.assertTrue(internal_picking) for move in internal_picking.move_ids: move.quantity = move.product_qty internal_picking.button_validate() self.assertEqual(internal_picking.state, 'done') self.assertEqual(order.order_line.qty_delivered, 0) def test_return_for_exchange_kit_product_component(self): """ Returning for exchange a kit's component should leave the original sale order line's qty_delivered with the correct value. """ for comp in self.bom_kit_1.bom_line_ids.product_id: self.env['stock.quant']._update_available_quantity(comp, self.company_data['default_warehouse'].lot_stock_id, quantity=10) comp_to_return = self.bom_kit_1.bom_line_ids.filtered(lambda bl: bl.product_qty == 1).product_id kit_product = self.kit_1 sale_order = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'order_line': [Command.create({ 'product_id': kit_product.id, 'product_uom_qty': 1.0, })], }) sale_order.action_confirm() delivery = sale_order.picking_ids delivery.action_assign() delivery.button_validate() return_picking_form = Form(self.env['stock.return.picking'].with_context(active_id=delivery.id, active_model='stock.picking')) return_wizard = return_picking_form.save() return_wizard.product_return_moves.filtered(lambda prm: prm.product_id == comp_to_return).quantity = 1 res = return_wizard.action_create_exchanges() return_picking = self.env['stock.picking'].browse(res['res_id']) return_picking.button_validate() exchange_picking = sale_order.picking_ids.filtered(lambda so: so.state != 'done') exchange_picking.button_validate() self.assertEqual(sale_order.order_line.qty_delivered, 1) def test_bidirectional_so_mo_link_with_mtso(self): """Test the link from the Manufacturing Order to the Sale Order when using the MTSO (Make To Stock or Make To Order) procurement method.""" # Set the MTO and Manufacture routes on the product route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id self.product_a.route_ids = [Command.set([route_manufacture.id, route_mto.id])] # Set the procure method to 'mts_else_mto' route_mto.rule_ids.filtered(lambda r: r.location_dest_id.usage == 'production').procure_method = 'mts_else_mto' # Create and confirm a Sale Order sale_order = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'order_line': [Command.create({ 'product_id': self.product_a.id, 'product_uom_qty': 1.0, })], }) sale_order.action_confirm() # Check the link between the SO and the MO self.assertEqual(sale_order.mrp_production_count, 1) mo = sale_order.mrp_production_ids self.assertEqual(mo.sale_order_count, 1) def test_so_with_kit_and_multiple_same_component(self): """Test that a Sale Order with a kit product containing multiple identical components can be confirmed, and that the picking is created correctly. Then verify that the Sale Order can be cancelled and re-confirmed, resulting in a new picking with moves properly linked to each BOM line. Finally, test returning a kit component for exchange.""" # Create a kit product with two identical components by duplicating the first BOM line self.bom_kit_1.bom_line_ids = self.bom_kit_1.bom_line_ids[0] self.env['mrp.bom.line'].create([ {'product_id': self.bom_kit_1.bom_line_ids[0].product_id.id, 'product_qty': 1.0, 'bom_id': self.bom_kit_1.id}, ]) # Create a Sale Order with the kit product so = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'order_line': [Command.create({ 'product_id': self.kit_1.id, 'product_uom_qty': 1.0, })], }) so.action_confirm() # Check that the picking has 2 moves for the same component picking = so.picking_ids self.assertEqual(len(picking.move_ids), 2, "There should be 2 moves for the same component in the picking") self.assertEqual(picking.move_ids.product_id, self.component_a, "All moves should be for the same component") self.assertEqual(picking.move_ids.bom_line_id, self.bom_kit_1.bom_line_ids, "Each move should be linked to a BOM line of the kit") # Cancel the Sale Order so._action_cancel() self.assertEqual(so.state, 'cancel', "The Sale Order should be cancelled") self.assertEqual(picking.state, 'cancel', "The picking should be cancelled when the Sale Order is cancelled") # Set the Sale Order back to draft and confirm again so.action_draft() so.action_confirm() # Check that a new picking is created with correct moves second_picking = so.picking_ids - picking self.assertEqual(len(second_picking.move_ids), 2, "The second picking should have 2 moves for the component") self.assertEqual(second_picking.move_ids.product_id, self.component_a, "All moves in the second picking should be for the same component") self.assertEqual(second_picking.move_ids.bom_line_id, self.bom_kit_1.bom_line_ids, "Each move in the second picking should be linked to a BOM line of the kit") # Returning for exchange a kit's component self.env['stock.quant']._update_available_quantity(self.component_a, self.company_data['default_warehouse'].lot_stock_id, quantity=10) second_picking.action_assign() second_picking.button_validate() return_picking_form = Form(self.env['stock.return.picking'].with_context(active_id=second_picking.id, active_model='stock.picking')) return_wizard = return_picking_form.save() return_wizard.product_return_moves.filtered(lambda prm: prm.product_id == self.component_a).quantity = 1 res = return_wizard.action_create_exchanges() return_picking = self.env['stock.picking'].browse(res['res_id']) return_picking.button_validate() exchange_picking = so.picking_ids.filtered(lambda so: so.state == 'assigned') exchange_picking.button_validate() so.order_line._compute_qty_delivered() # In the case where the kit has multiple identical components, only the first BOM line # is linked to all moves (this is a known limitation). self.assertEqual(exchange_picking.move_ids.bom_line_id, self.bom_kit_1.bom_line_ids[0], "All moves in the exchange picking should be linked to the first BOM line.") self.assertEqual(exchange_picking.move_ids.quantity, 2) def test_delivery_after_splitting_production(self): """ Test that processing the different MOs of a split production correctly updates the picking SM's quantity. """ # Set product up with MTO + Manufacture with (empty) BoM product = self._cls_create_product('Split Product', self.uom_unit, routes=[ self.company_data['default_warehouse'].mto_pull_id.route_id, self.company_data['default_warehouse'].manufacture_pull_id.route_id, ]) self.env['mrp.bom'].create({ 'product_tmpl_id': product.product_tmpl_id.id, 'product_uom_id': self.env.ref('uom.product_uom_unit').id, }) sale_order = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'order_line': [Command.create({ 'name': f"2 of {self.product.name}", 'product_id': product.id, 'product_uom_qty': 2, })], }) sale_order.action_confirm() sale_picking = sale_order.picking_ids self.assertTrue(sale_picking) mo = self.env['mrp.production'].search([('product_id', '=', product.id)], limit=1) action = mo.action_split() wizard = Form(self.env[action['res_model']].with_context(action['context'])) wizard.max_batch_size = 1 wizard.save().action_split() self.assertEqual(len(mo.production_group_id.production_ids), 2) mo.production_group_id.production_ids[0].button_mark_done() self.assertEqual(sale_picking.move_ids.quantity, 1) mo.production_group_id.production_ids[1].button_mark_done() self.assertEqual(sale_picking.move_ids.quantity, 2) sale_picking.button_validate() self.assertEqual(sale_order.order_line.qty_delivered, 2.0)