[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
This commit is contained in:
Arnaud Baes
2019-01-09 14:00:49 +00:00
parent 380b84bd09
commit b50d6a07db
4 changed files with 106 additions and 17 deletions
+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()
+40 -2
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):
@@ -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.
+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