[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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user