[IMP] mrp, sale_mrp: kits quantities

**Before this PR:**

- The quantities available when a kit was ordered was 0 even if enough components were available
- The quantities delivered when a kit was ordered were correct only if all move from the picking of its sale order were done. So the quantities delivered were correct only if all components were fully delivered.

**After this PR:**
- The number of kits available is computed based on the quantities available of the components needed to make the kit.
- The number of kits delivered is computed based on how many kits it's possible to make with the components already processed.
- Some test case are added the check the flows when kits are ordered.
Those test case make sure that:

  * 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 : 1915370

closes odoo/odoo#29270
This commit is contained in:
Robot Odoo
2019-01-09 16:28:07 +01:00
committed by GitHub
7 changed files with 993 additions and 42 deletions
+1
View File
@@ -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):
+6
View File
@@ -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:
+1
View File
@@ -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
+39
View File
@@ -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()
+82 -14
View File
@@ -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):
@@ -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):
@@ -49,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.
+838 -13
View File
@@ -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)
+26 -15
View File
@@ -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