From 380b84bd091cc9ce3b04ce7c178eac2a33333b0e Mon Sep 17 00:00:00 2001 From: Arnaud Baes Date: Tue, 4 Dec 2018 12:48:40 +0000 Subject: [PATCH 1/3] [IMP] sale_mrp: Kit quantities delivered Before this commit, the quantity_delivered of a kit was set to 0 unless all its components were delivered. With this commit, the quantity_delivered is the minimal quantity it's possible to make with the components currently delivered. TaskID: 1863856 --- addons/mrp/models/stock_move.py | 1 + addons/mrp/models/stock_rule.py | 6 ++++ addons/sale_mrp/models/sale_mrp.py | 54 +++++++++++++++++++++++------- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 610c1934b29..c7e884700ce 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -197,6 +197,7 @@ class StockMove(models.Model): 'quantity_done': quantity_done, 'state': 'draft', # will be confirmed below 'name': self.name, + 'bom_line_id': bom_line.id, } def _generate_move_phantom(self, bom_line, product_qty, quantity_done): diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index e06be0abd98..cdeca1b4db5 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -54,6 +54,11 @@ class StockRule(models.Model): subtype_id=self.env.ref('mail.mt_note').id) return True + def _get_custom_move_fields(self): + fields = super(StockRule, self)._get_custom_move_fields() + fields += ['bom_line_id'] + return fields + @api.multi def _get_matching_bom(self, product_id, values): if values.get('bom_id', False): @@ -108,6 +113,7 @@ class ProcurementGroup(models.Model): bom_line_uom = bom_line.product_uom_id quant_uom = bom_line.product_id.uom_id component_qty, procurement_uom = bom_line_uom._adjust_uom_quantities(bom_line_data['qty'], quant_uom) + values['bom_line_id'] = bom_line.id super(ProcurementGroup, self).run(bom_line.product_id, component_qty, procurement_uom, location_id, name, origin, values) return True else: diff --git a/addons/sale_mrp/models/sale_mrp.py b/addons/sale_mrp/models/sale_mrp.py index f1bdfc3f9de..fb471fd2823 100644 --- a/addons/sale_mrp/models/sale_mrp.py +++ b/addons/sale_mrp/models/sale_mrp.py @@ -10,20 +10,50 @@ class SaleOrderLine(models.Model): @api.multi def _compute_qty_delivered(self): + """ Computes the quantity delivered when a kit is sold. + To achieve this, this method fetch the quantities delivered of + each component of the kit and the quantity needed of each components to produce 1 kit. + Based on this, a ratio is computed for each component, + and the lowest one is kept to define the kit's quantity delivered. + """ super(SaleOrderLine, self)._compute_qty_delivered() - - for line in self: - if line.qty_delivered_method == 'stock_move': - # In the case of a kit, we need to check if all components are shipped. Since the BOM might - # have changed, we don't compute the quantities but verify the move state. - bom = self.env['mrp.bom']._bom_find(product=line.product_id) - if bom and bom.type == 'phantom': - moves = line.move_ids.filtered(lambda m: m.picking_id and m.picking_id.state != 'cancel') - bom_delivered = all([move.state == 'done' for move in moves]) - if bom_delivered: - line.qty_delivered = line.product_uom_qty + for order_line in self: + if order_line.qty_delivered_method == 'stock_move': + boms = order_line.move_ids.mapped('bom_line_id.bom_id') + # We fetch the BoMs of type kits linked to the order_line, + # the we keep only the one related to the finished produst. + # This bom shoud be the only one since bom_line_id was written on the moves + relevant_bom = boms.filtered(lambda b: b.type == 'phantom' and + (b.product_id == order_line.product_id or + (b.product_tmpl_id == order_line.product_id.product_tmpl_id and not b.product_id))) + if relevant_bom: + moves = order_line.move_ids.filtered(lambda m: m.state == 'done' and not m.scrapped) + qty_ratios = [] + order_uom_qty = order_line.product_uom._compute_quantity(order_line.product_uom_qty, relevant_bom.product_uom_id) + boms, bom_sub_lines = relevant_bom.explode(order_line.product_id, order_uom_qty) + for bom_line, bom_line_data in bom_sub_lines: + bom_line_moves = moves.filtered(lambda m: m.bom_line_id == bom_line) + if bom_line_moves: + # We compute the quantities needed of each components to make one kit. + # Then, we collect every relevant moves related to a specific component + # to know how many are considered delivered. + uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty'] + qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id) + delivery_moves = bom_line_moves.filtered(lambda m: m.location_dest_id.usage == 'customer' and not m.origin_returned_move_id or not (m.origin_returned_move_id and m.to_refund)) + return_moves = bom_line_moves.filtered(lambda m: m.location_dest_id.usage != 'customer' and m.to_refund) + qty_delivered = sum(delivery_moves.mapped('product_qty')) - sum(return_moves.mapped('product_qty')) + # We compute a ratio to know how many kits we can produce with this quantity of that specific component + qty_ratios.append(qty_delivered / qty_per_kit) + else: + qty_ratios.append(0.0) + break + if qty_ratios: + # Now that we have every ratio by components, we keep the lowest one to know how many kits we can produce + # with the quantities delivered of each component. We use the euclidean division here because a 'partial kit' + # doesn't make sense. + order_line.qty_delivered = min(qty_ratios) // 1 else: - line.qty_delivered = 0.0 + order_line.qty_delivered = 0.0 @api.multi def _get_bom_component_qty(self, bom): From b50d6a07db043a51aca6ab74235b9f63d7dcd176 Mon Sep 17 00:00:00 2001 From: Arnaud Baes Date: Tue, 4 Dec 2018 12:51:58 +0000 Subject: [PATCH 2/3] [IMP] sale_stock, sale_mrp: Kits quantities availability With this commit, the warning message triggered when there are not enough quantity available of a kit is based on the minimal quantity of a kit it's possible to make, based on the quantities available of its components. However, as this quantity is based on the 'virtual_available' quantity, the values could be negative, wich doesn't really make sense for a kit. This is why in such a case, the quantity available reported by the warning will be set to 0. TaskID: 1915370 --- addons/sale_mrp/models/__init__.py | 1 + addons/sale_mrp/models/product_product.py | 39 +++++++++++++++++++++ addons/sale_mrp/models/sale_mrp.py | 42 +++++++++++++++++++++-- addons/sale_stock/models/sale_order.py | 41 ++++++++++++++-------- 4 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 addons/sale_mrp/models/product_product.py diff --git a/addons/sale_mrp/models/__init__.py b/addons/sale_mrp/models/__init__.py index 75e0d652578..dce709157ba 100644 --- a/addons/sale_mrp/models/__init__.py +++ b/addons/sale_mrp/models/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import product_product from . import sale_mrp diff --git a/addons/sale_mrp/models/product_product.py b/addons/sale_mrp/models/product_product.py new file mode 100644 index 00000000000..ef00ceda2ff --- /dev/null +++ b/addons/sale_mrp/models/product_product.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Product(models.Model): + _inherit = "product.product" + + def _compute_quantities(self): + """ When the product is a kit, this override computes the fields : + - 'virtual_available' + - 'qty_available' + - 'incoming_qty' + - 'outgoing_qty' + """ + for product in self: + bom_kit = self.env['mrp.bom']._bom_find(product=product, bom_type='phantom') + if bom_kit: + boms, bom_sub_lines = bom_kit.explode(product, 1) + ratios_virtual_available = [] + ratios_qty_available = [] + ratios_incoming_qty = [] + ratios_outgoing_qty = [] + for bom_line, bom_line_data in bom_sub_lines: + component = bom_line.product_id + uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty'] + qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id) + ratios_virtual_available.append(component.virtual_available / qty_per_kit) + ratios_qty_available.append(component.qty_available / qty_per_kit) + ratios_incoming_qty.append(component.incoming_qty / qty_per_kit) + ratios_outgoing_qty.append(component.outgoing_qty / qty_per_kit) + if bom_sub_lines: + product.virtual_available = min(ratios_virtual_available) // 1 + product.qty_available = min(ratios_qty_available) // 1 + product.incoming_qty = min(ratios_incoming_qty) // 1 + product.outgoing_qty = min(ratios_incoming_qty) // 1 + else: + super(Product, self)._compute_quantities() diff --git a/addons/sale_mrp/models/sale_mrp.py b/addons/sale_mrp/models/sale_mrp.py index fb471fd2823..1c59e1a5757 100644 --- a/addons/sale_mrp/models/sale_mrp.py +++ b/addons/sale_mrp/models/sale_mrp.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models -from odoo.tools import float_compare +from odoo import api, fields, models, _ +from odoo.tools import float_compare, float_round class SaleOrderLine(models.Model): @@ -79,6 +79,44 @@ class SaleOrderLine(models.Model): components[product] = {'qty': qty, 'uom': to_uom.id} return components + def _check_availability(self, product_id): + """ If the 'product_id' is a kit, this method check if every component's + availability and catch every warning returned in order to merge them in a single + comprehensive warning + """ + bom_kit = self.env['mrp.bom']._bom_find(product=product_id, bom_type='phantom') + if not bom_kit: + return super(SaleOrderLine, self)._check_availability(product_id) + + kit = product_id + kit_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id) + + # We check if we need to display the quantities of each missing components for all warehouses + kit_by_wh = self.product_id.with_context(warehouse=self.order_id.warehouse_id.id) + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + if float_compare(kit_by_wh.virtual_available, kit_qty, precision_digits=precision) != -1: + return {} + ignore_warehouse = float_compare(kit.virtual_available, kit_qty, precision_digits=precision) != -1 + + message = '' + boms, bom_sub_lines = bom_kit.explode(kit, kit_qty) + for bom_line, bom_line_data in bom_sub_lines: + component = bom_line.product_id + component_uom_qty = bom_line_data['qty'] + component_qty = bom_line.product_uom_id._compute_quantity(component_uom_qty, bom_line.product_id.uom_id) + component_warning = self._check_availability_warning(component, component_qty, ignore_warehouse=ignore_warehouse) + if component_warning: + message += component_warning['warning']['message'] + '\n' + + warning = {} + if message: + warning_mess = { + 'title': _('Not enough inventory!'), + 'message': message + } + warning = {'warning': warning_mess} + return warning + def _get_qty_procurement(self): self.ensure_one() # Specific case when we change the qty on a SO for a kit product. diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index c207ec6195b..8658f2d0290 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -256,29 +256,41 @@ class SaleOrderLine(models.Model): if not self.product_id or not self.product_uom_qty or not self.product_uom: self.product_packaging = False return {} - if self.product_id.type == 'product': + return self._check_availability(self.product_id) + + def _check_availability(self, product_id): + """ The purpose of this method is only to be overriden if + 'product_id' is a kit + """ + product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id) + return self._check_availability_warning(product_id, product_qty) + + def _check_availability_warning(self, product_id, product_qty, ignore_warehouse=False): + if product_id.type == 'product': precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - product = self.product_id.with_context( + product_by_wh = product_id.with_context( warehouse=self.order_id.warehouse_id.id, lang=self.order_id.partner_id.lang or self.env.user.lang or 'en_US' ) - product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id) - if float_compare(product.virtual_available, product_qty, precision_digits=precision) == -1: - is_available = self._check_routing() + if float_compare(product_by_wh.virtual_available, product_qty, precision_digits=precision) == -1: + is_available = self._check_routing(product_id) if not is_available: - message = _('You plan to sell %s %s of %s but you only have %s %s available in %s warehouse.') % \ - (self.product_uom_qty, self.product_uom.name, self.product_id.name, product.virtual_available, product.uom_id.name, self.order_id.warehouse_id.name) + message = _('You plan to sell %s %s of %s but you only have %s %s available in %s warehouse.') % \ + (self.product_uom_qty, self.product_uom.name, product_id.name, + product_by_wh.virtual_available, + product_by_wh.uom_id.name, self.order_id.warehouse_id.name) # We check if some products are available in other warehouses. - if float_compare(product.virtual_available, self.product_id.virtual_available, precision_digits=precision) == -1: + if not ignore_warehouse and float_compare(product_by_wh.virtual_available, product_id.virtual_available, + precision_digits=precision) == -1: message += _('\nThere are %s %s available across all warehouses.\n\n') % \ - (self.product_id.virtual_available, product.uom_id.name) + (product_id.virtual_available, product_by_wh.uom_id.name) for warehouse in self.env['stock.warehouse'].search([]): - quantity = self.product_id.with_context(warehouse=warehouse.id).virtual_available + quantity = product_id.with_context(warehouse=warehouse.id).virtual_available if quantity > 0: - message += "%s: %s %s\n" % (warehouse.name, quantity, self.product_id.uom_id.name) + message += "%s: %s %s\n" % (warehouse.name, quantity, product_id.uom_id.name) warning_mess = { 'title': _('Not enough inventory!'), - 'message' : message + 'message': message } return {'warning': warning_mess} return {} @@ -399,14 +411,13 @@ class SaleOrderLine(models.Model): } return {} - - def _check_routing(self): + def _check_routing(self, product_id): """ Verify the route of the product based on the warehouse return True if the product availibility in stock does not need to be verified, which is the case in MTO, Cross-Dock or Drop-Shipping """ is_available = False - product_routes = self.route_id or (self.product_id.route_ids + self.product_id.categ_id.total_route_ids) + product_routes = self.route_id or (product_id.route_ids + product_id.categ_id.total_route_ids) # Check MTO wh_mto_route = self.order_id.warehouse_id.mto_pull_id.route_id From 82fd418a98377a5b62484f753784ecbb1468716d Mon Sep 17 00:00:00 2001 From: Arnaud Baes Date: Tue, 4 Dec 2018 12:55:03 +0000 Subject: [PATCH 3/3] [IMP] sale_mrp: Test cases added for kits flows Thoses tests checks the correctness of the kit's flows. The following cases are tested : Delivered quantities are accurate if : - kits in kit - backorders are made - returns are made - returns of returns are made - Multiple differents UoMs are setted on the components of a kit Available quantities are accurate if - Kits in kit - Multiple differents UoMs are setted on the components of a kit TaskID: 1863856 --- addons/sale_mrp/tests/test_sale_mrp_flow.py | 851 +++++++++++++++++++- 1 file changed, 838 insertions(+), 13 deletions(-) diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 5b23d94dfb1..9f353766ab9 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -47,7 +47,110 @@ class TestSaleMrpFlow(common.TransactionCase): 'uom_type': 'bigger', 'rounding': 0.001}) - def create_product(self, name, uom_id, routes=()): + # Creating all components + self.component_a = self._create_product('Comp A', self.uom_unit) + self.component_b = self._create_product('Comp B', self.uom_unit) + self.component_c = self._create_product('Comp C', self.uom_unit) + self.component_d = self._create_product('Comp D', self.uom_unit) + self.component_e = self._create_product('Comp E', self.uom_unit) + self.component_f = self._create_product('Comp F', self.uom_unit) + self.component_g = self._create_product('Comp G', self.uom_unit) + + # Create a kit 'kit_1' : + # ----------------------- + # + # kit_1 --|- component_a x2 + # |- component_b x1 + # |- component_c x3 + + self.kit_1 = self._create_product('Kit 1', self.uom_unit) + + self.bom_kit_1 = self.env['mrp.bom'].create({ + 'product_tmpl_id': self.kit_1.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine = self.env['mrp.bom.line'] + BomLine.create({ + 'product_id': self.component_a.id, + 'product_qty': 2.0, + 'bom_id': self.bom_kit_1.id}) + BomLine.create({ + 'product_id': self.component_b.id, + 'product_qty': 1.0, + 'bom_id': self.bom_kit_1.id}) + BomLine.create({ + 'product_id': self.component_c.id, + 'product_qty': 3.0, + 'bom_id': self.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 + self.kit_2 = self._create_product('Kit 2', self.uom_unit) + self.kit_3 = self._create_product('kit 3', self.uom_unit) + self.kit_parent = self._create_product('Kit Parent', self.uom_unit) + + # Linking the kits and the components via some 'phantom' BoMs + bom_kit_2 = self.env['mrp.bom'].create({ + 'product_tmpl_id': self.kit_2.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine.create({ + 'product_id': self.component_d.id, + 'product_qty': 1.0, + 'bom_id': bom_kit_2.id}) + BomLine.create({ + 'product_id': self.kit_1.id, + 'product_qty': 2.0, + 'bom_id': bom_kit_2.id}) + + bom_kit_parent = self.env['mrp.bom'].create({ + 'product_tmpl_id': self.kit_parent.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine.create({ + 'product_id': self.component_e.id, + 'product_qty': 1.0, + 'bom_id': bom_kit_parent.id}) + BomLine.create({ + 'product_id': self.kit_2.id, + 'product_qty': 2.0, + 'bom_id': bom_kit_parent.id}) + + bom_kit_3 = self.env['mrp.bom'].create({ + 'product_tmpl_id': self.kit_3.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine.create({ + 'product_id': self.component_f.id, + 'product_qty': 1.0, + 'bom_id': bom_kit_3.id}) + BomLine.create({ + 'product_id': self.component_g.id, + 'product_qty': 2.0, + 'bom_id': bom_kit_3.id}) + + BomLine.create({ + 'product_id': self.kit_3.id, + 'product_qty': 2.0, + 'bom_id': bom_kit_parent.id}) + + def _create_product(self, name, uom_id, routes=()): p = Form(self.env['product.product']) p.name = name p.type = 'product' @@ -58,6 +161,58 @@ class TestSaleMrpFlow(common.TransactionCase): p.route_ids.add(r) return p.save() + # Helper to process quantities based on a dict following this structure : + # + # qty_to_process = { + # product_id: qty + # } + + def _process_quantities(self, moves, quantities_to_process): + """ Helper to process quantities based on a dict following this structure : + qty_to_process = { + product_id: qty + } + """ + moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys()) + for move in moves_to_process: + move.write({'quantity_done': quantities_to_process[move.product_id]}) + + def _assert_quantities(self, moves, quantities_to_process): + """ Helper to check expected quantities based on a dict following this structure : + qty_to_process = { + product_id: qty + ... + } + """ + moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys()) + for move in moves_to_process: + self.assertEquals(move.product_uom_qty, quantities_to_process[move.product_id]) + + def _create_move_quantities(self, qty_to_process, components, warehouse): + """ Helper to creates moves in order to update the quantities of components + on a specific warehouse. This ensure that all compute fields are triggered. + The structure of qty_to_process should be the following : + + qty_to_process = { + component: (qty, uom), + ... + } + """ + for comp in components: + f = Form(self.env['stock.move']) + f.name = 'Test Receipt Components' + f.location_id = self.env.ref('stock.stock_location_suppliers') + f.location_dest_id = warehouse.lot_stock_id + f.product_id = comp + f.product_uom = qty_to_process[comp][1] + f.product_uom_qty = qty_to_process[comp][0] + move = f.save() + move._action_confirm() + move._action_assign() + move_line = move.move_line_ids[0] + move_line.qty_done = qty_to_process[comp][0] + move._action_done() + def test_00_sale_mrp_flow(self): """ Test sale to mrp flow with diffrent unit of measure.""" @@ -66,10 +221,10 @@ class TestSaleMrpFlow(common.TransactionCase): # -------------------------- route_manufacture = self.warehouse.manufacture_pull_id.route_id route_mto = self.warehouse.mto_pull_id.route_id - product_a = self.create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto]) - product_c = self.create_product('Product C', self.uom_kg) - product_b = self.create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto]) - product_d = self.create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto]) + product_a = self._create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto]) + product_c = self._create_product('Product C', self.uom_kg) + product_b = self._create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto]) + product_d = self._create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto]) # ------------------------------------------------------------------------------------------ # Bill of materials for product A, B, D. @@ -493,7 +648,665 @@ class TestSaleMrpFlow(common.TransactionCase): 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.env.ref('stock.stock_location_stock') + 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.ref('base.res_partner_1') + 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.assertEquals(len(so.picking_ids), 1) + picking_original = so.picking_ids[0] + move_lines = picking_original.move_lines + + # Check if the correct amount of stock.moves are created + self.assertEquals(len(move_lines), 3) + + # Check if BoM is created and is for a 'Kit' + bom_from_k1 = self.env['mrp.bom']._bom_find(product=self.kit_1) + self.assertEquals(self.bom_kit_1.id, bom_from_k1.id) + self.assertEquals(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.assertEquals(len(order_lines), 1) + order_line = order_lines[0] + self.assertEquals(order_line.product_id.id, self.kit_1.id) + self.assertEquals(order_line.product_uom_qty, 10.0) + + # Check if correct qty is ordered for each component of the kit + expected_quantities = { + self.component_a: 20, + self.component_b: 10, + self.component_c: 30, + } + self._assert_quantities(move_lines, expected_quantities) + + # Process only x1 of the first component then create a backorder for the missing components + picking_original.move_lines[0].write({'quantity_done': 1}) + backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, so.picking_ids[0].id)]}) + backorder_wizard.process() + + # Check that the backorder was created, no kit should be delivered at this point + self.assertEquals(len(so.picking_ids), 2) + backorder_1 = so.picking_ids - picking_original + self.assertEquals(backorder_1.backorder_id.id, picking_original.id) + self.assertEquals(order_line.qty_delivered, 0) + + # Process only x6 each componenent in the picking + # Then create a backorder for the missing components + backorder_1.move_lines.write({'quantity_done': 6}) + backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, backorder_1.id)]}) + backorder_wizard.process() + + # Check that a backorder is created + self.assertEquals(len(so.picking_ids), 3) + backorder_2 = so.picking_ids - picking_original - backorder_1 + self.assertEquals(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.assertEquals(order_line.qty_delivered, 2) + + # Process x3 more unit of each components : + # - Now only 3 kits should be delivered + # - A backorder will be created, the SO should have 3 picking_ids linked to it. + backorder_2.move_lines.write({'quantity_done': 3}) + + backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, backorder_2.id)]}) + backorder_wizard.process() + + self.assertEquals(len(so.picking_ids), 4) + backorder_3 = so.picking_ids - picking_original - backorder_2 - backorder_1 + self.assertEquals(backorder_3.backorder_id.id, backorder_2.id) + self.assertEquals(order_line.qty_delivered, 3) + + # Adding missing components + qty_to_process = { + self.component_a: 10, + self.component_b: 1, + self.component_c: 21, + } + self._process_quantities(backorder_3.move_lines, qty_to_process) + + # Validating the last backorder now it's complete + backorder_3.button_validate() + order_line._compute_qty_delivered() + + # All kits should be delivered + self.assertEquals(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.env.ref('stock.stock_location_stock') + 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.ref('base.res_partner_1') + 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.assertEquals(len(so.picking_ids), 1) + order_line = so.order_line[0] + picking_original = so.picking_ids[0] + move_lines = picking_original.move_lines + products = move_lines.mapped('product_id') + kits = [self.kit_parent, self.kit_3, self.kit_2, self.kit_1] + components = [self.component_a, self.component_b, self.component_c, self.component_d, self.component_e, self.component_f, self.component_g] + expected_quantities = { + self.component_a: 56.0, + self.component_b: 28.0, + self.component_c: 84.0, + self.component_d: 14.0, + self.component_e: 7.0, + self.component_f: 14.0, + self.component_g: 28.0 + } + + self.assertEquals(len(move_lines), 7) + self.assertTrue(not any(kit in products for kit in kits)) + self.assertTrue(all(component in products for component in components)) + self._assert_quantities(move_lines, expected_quantities) + + # Process only 7 units of each component + qty_to_process = 7 + move_lines.write({'quantity_done': qty_to_process}) + + # Create a backorder for the missing componenents + backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, so.picking_ids[0].id)]}) + backorder_wizard.process() + + # Check that a backorded is created + self.assertEquals(len(so.picking_ids), 2) + backorder_1 = so.picking_ids - picking_original + self.assertEquals(backorder_1.backorder_id.id, picking_original.id) + + # Even if some components are delivered completely, + # no KitParent should be delivered + self.assertEquals(order_line.qty_delivered, 0) + + # Process just enough components to make 1 kit_parent + qty_to_process = { + self.component_a: 1, + self.component_c: 5, + } + self._process_quantities(backorder_1.move_lines, qty_to_process) + + # Create a backorder for the missing componenents + backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, backorder_1.id)]}) + backorder_wizard.process() + + # Only 1 kit_parent should be delivered at this point + self.assertEquals(order_line.qty_delivered, 1) + + # Check that the second backorder is created + self.assertEquals(len(so.picking_ids), 3) + backorder_2 = so.picking_ids - picking_original - backorder_1 + self.assertEquals(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.assertEquals(len(backorder_2.move_lines), 6) + move_comp_e = backorder_2.move_lines.filtered(lambda m: m.product_id.id == self.component_e.id) + self.assertFalse(move_comp_e) + self._assert_quantities(backorder_2.move_lines, expected_quantities) + + # Process enough components to make x3 kit_parents + qty_to_process = { + self.component_a: 16, + self.component_b: 5, + self.component_c: 24, + self.component_g: 5 + } + self._process_quantities(backorder_2.move_lines, qty_to_process) + + # Create a backorder for the missing componenents + backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, backorder_2.id)]}) + backorder_wizard.process() + + # Check that x3 kit_parents are indeed delivered + self.assertEquals(order_line.qty_delivered, 3) + + # Check that the third backorder is created + self.assertEquals(len(so.picking_ids), 4) + backorder_3 = so.picking_ids - (picking_original + backorder_1 + backorder_2) + self.assertEquals(backorder_3.backorder_id.id, backorder_2.id) + + # Check the components quantities that backorder_3 should have + expected_quantities = { + self.component_a: 32, + self.component_b: 16, + self.component_c: 48, + self.component_d: 7, + self.component_f: 7, + self.component_g: 16 + } + self._assert_quantities(backorder_3.move_lines, expected_quantities) + + # Process all missing components + self._process_quantities(backorder_3.move_lines, expected_quantities) + + # Validating the last backorder now it's complete. + # All kits should be delivered + backorder_3.button_validate() + self.assertEquals(order_line.qty_delivered, 7.0) + + # Return all components processed by backorder_3 + StockReturnPicking = self.env['stock.return.picking'] + default_data = StockReturnPicking.with_context(active_ids=backorder_3.ids, active_id=backorder_3.ids[0]).default_get( + ['move_dest_exists', 'original_location_id', 'product_return_moves', 'parent_location_id', 'location_id']) + return_wiz = StockReturnPicking.with_context(active_ids=backorder_3.ids, active_id=backorder_3.ids[0]).create(default_data) + for return_move in return_wiz.product_return_moves: + return_move.write({ + 'quantity': expected_quantities[return_move.product_id], + 'to_refund': True + }) + res = return_wiz.create_returns() + return_pick = self.env['stock.picking'].browse(res['res_id']) + + # Process all components and validate the picking + wiz_act = return_pick.button_validate() + wiz = self.env[wiz_act['res_model']].browse(wiz_act['res_id']) + wiz.process() + + # Now quantity delivered should be 3 again + self.assertEquals(order_line.qty_delivered, 3) + + default_data = StockReturnPicking.with_context(active_ids=return_pick.ids, active_id=return_pick.ids[0]).default_get( + ['move_dest_exists', 'original_location_id', 'product_return_moves', 'parent_location_id', 'location_id']) + return_wiz = StockReturnPicking.with_context(active_ids=return_pick.ids, active_id=return_pick.ids[0]).create( + default_data) + for move in return_wiz.product_return_moves: + move.quantity = expected_quantities[move.product_id] + res = return_wiz.create_returns() + return_of_return_pick = self.env['stock.picking'].browse(res['res_id']) + + # Process all components except one of each + for move in return_of_return_pick.move_lines: + move.write({ + 'quantity_done': expected_quantities[move.product_id] - 1, + 'to_refund': False + }) + + backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, return_of_return_pick.id)]}) + backorder_wizard.process() + + # As one of each component is missing, only 6 kit_parents should be delivered + self.assertEquals(order_line.qty_delivered, 6) + + # Check that the 4th backorder is created. + self.assertEquals(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.assertEquals(backorder_4.backorder_id.id, return_of_return_pick.id) + + # Check the components quantities that backorder_4 should have + for move in backorder_4.move_lines: + self.assertEquals(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.ref('base.res_partner_1') + f.warehouse_id = warehouse_2 + with f.order_line.new() as line: + line.product_id = self.kit_parent + line.product_uom_qty = qty_ordered + so = f.save() + order_line = so.order_line[0] + + # Check that not enough enough quantities are available in the warehouse set in the SO + # but there are enough quantities in Warehouse 1 for 1 kit_parent + kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id) + kit_parent_wh1 = self.kit_parent.with_context(warehouse=warehouse_1.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.assertEquals(kit_parent_wh_order.virtual_available, 0) + self.assertEquals(kit_parent_wh1.virtual_available, 1) + + # A warning message should be returned as there arn't enough quantities available for the sale order + warning = order_line._onchange_product_id_check_availability() + self.assertTrue(warning) + + # We receive enoug of each component in Warehouse 2 to make 3 kit_parent + qty_to_process = { + self.component_a: (17, self.uom_unit), + self.component_b: (12, self.uom_unit), + self.component_c: (25, self.uom_unit), + self.component_d: (5, self.uom_unit), + self.component_e: (2, self.uom_unit), + self.component_f: (5, self.uom_unit), + self.component_g: (8, self.uom_unit), + } + self._create_move_quantities(qty_to_process, components, warehouse_2) + + # As 'Warehouse 2' is the warehouse linked to the SO, 3 kits should be available + # But the quantity available in Warehouse 1 should stay 1 + kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id) + kit_parent_wh1 = self.kit_parent.with_context(warehouse=warehouse_1.id) + self.assertEquals(kit_parent_wh_order.virtual_available, 3) + self.assertEquals(kit_parent_wh1.virtual_available, 1) + + # A warning message should be returned as there arn't enough quantities available for the sale order + warning = order_line._onchange_product_id_check_availability() + self.assertTrue(warning) + + # We receive enough of each component in Warehouse 2 to make 7 kit_parent + qty_to_process = { + self.component_a: (32, self.uom_unit), + self.component_b: (16, self.uom_unit), + self.component_c: (48, self.uom_unit), + self.component_d: (8, self.uom_unit), + self.component_e: (4, self.uom_unit), + self.component_f: (8, self.uom_unit), + self.component_g: (16, self.uom_unit), + } + self._create_move_quantities(qty_to_process, components, warehouse_2) + + # Enough quantities should be available, no warning message should be displayed + kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id) + self.assertEquals(kit_parent_wh_order.virtual_available, 7) + + # Some cache issue prevents the following to work in a test + #warning = order_line._onchange_product_id_check_availability() + #self.assertFalse(warning) + + def test_06_kit_qty_delivered_mixed_uom(self): + """ + Check that the quantities delivered are correct when a kit involves + multiple UoMs on its components + """ + # Create some components + component_uom_unit = self._create_product('Comp Unit', self.uom_unit) + component_uom_dozen = self._create_product('Comp Dozen', self.uom_dozen) + component_uom_kg = self._create_product('Comp Kg', self.uom_kg) + + # Create a kit 'kit_uom_1' : + # ----------------------- + # + # kit_uom_1 --|- component_uom_unit x2 Test-Dozen + # |- component_uom_dozen x1 Test-Dozen + # |- component_uom_kg x3 Test-G + + kit_uom_1 = self._create_product('Kit 1', self.uom_unit) + + bom_kit_uom_1 = self.env['mrp.bom'].create({ + 'product_tmpl_id': kit_uom_1.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine = self.env['mrp.bom.line'] + BomLine.create({ + 'product_id': component_uom_unit.id, + 'product_qty': 2.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_1.id}) + BomLine.create({ + 'product_id': component_uom_dozen.id, + 'product_qty': 1.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_1.id}) + BomLine.create({ + 'product_id': component_uom_kg.id, + 'product_qty': 3.0, + 'product_uom_id': self.uom_gm.id, + 'bom_id': bom_kit_uom_1.id}) + + # Updating the quantities in stock to prevent + # a 'Not enough inventory' warning message. + stock_location = self.env.ref('stock.stock_location_stock') + 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.ref('base.res_partner_1') + f = Form(self.env['sale.order']) + f.partner_id = partner + with f.order_line.new() as line: + line.product_id = kit_uom_1 + line.product_uom_qty = 10.0 + + so = f.save() + so.action_confirm() + + picking_original = so.picking_ids[0] + move_lines = picking_original.move_lines + order_line = so.order_line[0] + + # Check that the quantities on the picking are the one expected for each components + for ml in move_lines: + corr_bom_line = bom_kit_uom_1.bom_line_ids.filtered(lambda b: b.product_id.id == ml.product_id.id) + computed_qty = ml.product_uom._compute_quantity(ml.product_uom_qty, corr_bom_line.product_uom_id) + self.assertEquals(computed_qty, order_line.product_uom_qty * corr_bom_line.product_qty) + + # Processe enough componenents in the picking to make 2 kit_uom_1 + # Then create a backorder for the missing components + qty_to_process = { + component_uom_unit: 48, + component_uom_dozen: 3, + component_uom_kg: 0.006 + } + self._process_quantities(move_lines, qty_to_process) + backorder_wizard = self.env['stock.backorder.confirmation'].create( + {'pick_ids': [(4, so.picking_ids[0].id)]}) + backorder_wizard.process() + + # Check that a backorder is created + self.assertEquals(len(so.picking_ids), 2) + backorder_1 = so.picking_ids - picking_original + self.assertEquals(backorder_1.backorder_id.id, picking_original.id) + + # Only 2 kits should be delivered + self.assertEquals(order_line.qty_delivered, 2) + + # Adding missing components + qty_to_process = { + component_uom_unit: 192, + component_uom_dozen: 7, + component_uom_kg: 0.024 + } + self._process_quantities(backorder_1.move_lines, qty_to_process) + + # Validating the last backorder now it's complete + backorder_1.button_validate() + order_line._compute_qty_delivered() + # All kits should be delivered + self.assertEquals(order_line.qty_delivered, 10) + + @mute_logger('odoo.tests.common.onchange') + def test_07_kit_availability_mixed_uom(self): + """ + Check that the 'Not enough inventory' warning message displays correct + informations when a kit with multiple UoMs on its components is ordered + """ + + # Create some components + component_uom_unit = self._create_product('Comp Unit', self.uom_unit) + component_uom_dozen = self._create_product('Comp Dozen', self.uom_dozen) + component_uom_kg = self._create_product('Comp Kg', self.uom_kg) + component_uom_gm = self._create_product('Comp g', self.uom_gm) + components = [component_uom_unit, component_uom_dozen, component_uom_kg, component_uom_gm] + + # Create a kit 'kit_uom_in_kit' : + # ----------------------- + # kit_uom_in_kit --|- component_uom_gm x3 Test-KG + # |- kit_uom_1 x2 Test-Dozen --|- component_uom_unit x2 Test-Dozen + # |- component_uom_dozen x1 Test-Dozen + # |- component_uom_kg x3 Test-G + + kit_uom_1 = self._create_product('Sub Kit 1', self.uom_unit) + kit_uom_in_kit = self._create_product('Parent Kit', self.uom_unit) + + bom_kit_uom_1 = self.env['mrp.bom'].create({ + 'product_tmpl_id': kit_uom_1.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom'}) + + BomLine = self.env['mrp.bom.line'] + BomLine.create({ + 'product_id': component_uom_unit.id, + 'product_qty': 2.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_1.id}) + BomLine.create({ + 'product_id': component_uom_dozen.id, + 'product_qty': 1.0, + 'product_uom_id': self.uom_dozen.id, + 'bom_id': bom_kit_uom_1.id}) + BomLine.create({ + 'product_id': component_uom_kg.id, + 'product_qty': 3.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.072) + 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.ref('base.res_partner_1') + f.warehouse_id = warehouse_1 + with f.order_line.new() as line: + line.product_id = kit_uom_in_kit + line.product_uom_qty = qty_ordered + + so = f.save() + order_line = so.order_line[0] + + # Check that not enough enough quantities are available in the warehouse set in the SO + # but there are enough quantities in Warehouse 1 for 1 kit_parent + kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities() + virtual_available_wh_order = kit_uom_in_kit.virtual_available + self.assertEquals(virtual_available_wh_order, 1) + + # A warning message should be returned as there arn't enough quantities available for the sale order + warning = order_line._onchange_product_id_check_availability() + self.assertTrue(warning) + + # 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.144, self.uom_kg), + component_uom_gm: (6000, self.uom_gm) + } + self._create_move_quantities(qty_to_process, components, warehouse_1) + + # Enough quantities should be available to make 3 kit_uom_in_kit and a warning message should be displayed + warning = order_line._onchange_product_id_check_availability() + self.assertTrue(warning) + kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities() + virtual_available_wh_order = kit_uom_in_kit.virtual_available + self.assertEquals(virtual_available_wh_order, 3) + + # We process enough quantities to have enough kit_uom_in_kit available for the sale order. + self._create_move_quantities(qty_to_process, components, warehouse_1) + + # We check that enough quantities were processed to sell 5 kit_uom_in_kit + kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities() + self.assertEquals(kit_uom_in_kit.virtual_available, 5) + + # Some cache issue prevents the following to work in a test + + # No warning should be raised + # warning = order_line._onchange_product_id_check_availability() + # self.assertFalse(warning) + def test_10_sale_mrp_kits_routes(self): # Create a kit 'kit_1' : @@ -502,9 +1315,9 @@ class TestSaleMrpFlow(common.TransactionCase): # kit_1 --|- component_shelf1 x3 # |- component_shelf2 x2 - kit_1 = self.create_product('Kit1', self.uom_unit) - component_shelf1 = self.create_product('Comp Shelf1', self.uom_unit) - component_shelf2 = self.create_product('Comp Shelf2', self.uom_unit) + kit_1 = self._create_product('Kit1', self.uom_unit) + component_shelf1 = self._create_product('Comp Shelf1', self.uom_unit) + component_shelf2 = self._create_product('Comp Shelf2', self.uom_unit) with Form(self.env['mrp.bom']) as bom: bom.product_tmpl_id = kit_1.product_tmpl_id @@ -550,6 +1363,10 @@ class TestSaleMrpFlow(common.TransactionCase): 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.env.ref('stock.warehouse0').lot_stock_id, 15) + self.env['stock.quant']._update_available_quantity(component_shelf2, self.env.ref('stock.warehouse0').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.ref('base.res_partner_2') @@ -573,7 +1390,6 @@ class TestSaleMrpFlow(common.TransactionCase): self.assertEquals(move_shelf2.location_id.id, self.ref('stock.stock_location_14')) self.assertEquals(move_shelf2.location_dest_id.id, self.ref('stock.stock_location_customers')) - @mute_logger('odoo.tests.common.onchange') def test_11_sale_mrp_explode_kits_uom_quantities(self): # Create a kit 'kit_1' : @@ -582,9 +1398,9 @@ class TestSaleMrpFlow(common.TransactionCase): # 2x Dozens kit_1 --|- component_unit x6 Units # |- component_kg x7 Kg - kit_1 = self.create_product('Kit1', self.uom_unit) - component_unit = self.create_product('Comp Unit', self.uom_unit) - component_kg = self.create_product('Comp Kg', self.uom_kg) + kit_1 = self._create_product('Kit1', self.uom_unit) + component_unit = self._create_product('Comp Unit', self.uom_unit) + component_kg = self._create_product('Comp Kg', self.uom_kg) with Form(self.env['mrp.bom']) as bom: bom.product_tmpl_id = kit_1.product_tmpl_id @@ -600,9 +1416,19 @@ class TestSaleMrpFlow(common.TransactionCase): 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.ref('base.res_partner_2') + order_form.warehouse_id = warehouse_1 with order_form.order_line.new() as line: line.product_id = kit_1 line.product_uom = self.uom_unit @@ -620,4 +1446,3 @@ class TestSaleMrpFlow(common.TransactionCase): move_component_kg = order.picking_ids[0].move_lines - move_component_unit self.assertEquals(move_component_unit.product_uom_qty, 0.5) self.assertEquals(move_component_kg.product_uom_qty, 0.583) -