diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py
index 325b303458a..7b4982b27bd 100644
--- a/addons/mrp/models/product.py
+++ b/addons/mrp/models/product.py
@@ -64,6 +64,17 @@ class ProductProduct(models.Model):
for product in self:
product.used_in_bom_count = self.env['mrp.bom'].search_count([('bom_line_ids.product_id', '=', product.id)])
+ def get_components(self):
+ """ Return the components list ids in case of kit product.
+ Return the product itself otherwise"""
+ self.ensure_one()
+ bom_kit = self.env['mrp.bom']._bom_find(product=self, bom_type='phantom')
+ if bom_kit:
+ boms, bom_sub_lines = bom_kit.explode(self, 1)
+ return [bom_line.product_id.id for bom_line, data in bom_sub_lines if bom_line.product_id.type == 'product']
+ else:
+ return super(ProductProduct, self).get_components()
+
def action_used_in_bom(self):
self.ensure_one()
action = self.env.ref('mrp.mrp_bom_form_action').read()[0]
@@ -101,6 +112,9 @@ class ProductProduct(models.Model):
ratios_free_qty = []
for bom_line, bom_line_data in bom_sub_lines:
component = bom_line.product_id
+ if component.type != 'product':
+ # Consumable product have 0 qty_available so we exclude them
+ continue
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)
@@ -108,7 +122,7 @@ class ProductProduct(models.Model):
ratios_incoming_qty.append(component.incoming_qty / qty_per_kit)
ratios_outgoing_qty.append(component.outgoing_qty / qty_per_kit)
ratios_free_qty.append(component.free_qty / qty_per_kit)
- if bom_sub_lines:
+ if bom_sub_lines and ratios_virtual_available: # Guard against all cnsumable bom: at least one ratio should be present.
product.virtual_available = min(ratios_virtual_available) // 1
product.qty_available = min(ratios_qty_available) // 1
product.incoming_qty = min(ratios_incoming_qty) // 1
diff --git a/addons/sale_mrp/models/sale_mrp.py b/addons/sale_mrp/models/sale_mrp.py
index 33f3bfc40f8..8bba330ca30 100644
--- a/addons/sale_mrp/models/sale_mrp.py
+++ b/addons/sale_mrp/models/sale_mrp.py
@@ -8,6 +8,16 @@ from odoo.tools import float_compare, float_round
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
+ @api.depends('product_uom_qty', 'qty_delivered', 'product_id', 'state')
+ def _compute_qty_to_deliver(self):
+ """The inventory widget should now be visible in more cases if the product is consumable."""
+ super(SaleOrderLine, self)._compute_qty_to_deliver()
+ for line in self:
+ if line.state == 'draft' and line.product_type == 'consu':
+ components = line.product_id.get_components()
+ if components and components != [line.product_id.id]:
+ line.display_qty_widget = True
+
def _compute_qty_delivered(self):
super(SaleOrderLine, self)._compute_qty_delivered()
for order_line in self:
@@ -51,44 +61,6 @@ 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, previous_product_uom_qty=False):
self.ensure_one()
# Specific case when we change the qty on a SO for a kit product.
diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py
index bafa2a17882..a849b8e82c4 100644
--- a/addons/sale_mrp/tests/test_sale_mrp_flow.py
+++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py
@@ -5,7 +5,7 @@ from datetime import datetime
from odoo.tests import common, Form
from odoo.exceptions import UserError
-from odoo.tools import mute_logger
+from odoo.tools import mute_logger, float_compare
class TestSaleMrpFlow(common.SavepointCase):
@@ -1052,9 +1052,8 @@ class TestSaleMrpFlow(common.SavepointCase):
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)
+ # Check there arn't enough quantities available for the sale order
+ self.assertTrue(float_compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) == -1)
# We receive enoug of each component in Warehouse 2 to make 3 kit_parent
qty_to_process = {
@@ -1075,9 +1074,8 @@ class TestSaleMrpFlow(common.SavepointCase):
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)
+ # Check there arn't enough quantities available for the sale order
+ self.assertTrue(float_compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) == -1)
# We receive enough of each component in Warehouse 2 to make 7 kit_parent
qty_to_process = {
@@ -1095,10 +1093,6 @@ class TestSaleMrpFlow(common.SavepointCase):
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
@@ -1294,9 +1288,8 @@ class TestSaleMrpFlow(common.SavepointCase):
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)
+ # Check there arn't enough quantities available for the sale order
+ self.assertTrue(float_compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) == -1)
# We receive enough of each component in Warehouse 1 to make 3 kit_uom_in_kit.
# Moves are created instead of only updating the quant quantities in order to trigger every compute fields.
@@ -1308,9 +1301,8 @@ class TestSaleMrpFlow(common.SavepointCase):
}
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)
+ # Check there arn't enough quantities available for the sale order
+ self.assertTrue(float_compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) == -1)
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)
@@ -1322,12 +1314,6 @@ class TestSaleMrpFlow(common.SavepointCase):
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' :
diff --git a/addons/sale_stock/__manifest__.py b/addons/sale_stock/__manifest__.py
index e078cc8ea80..aa1e6be8563 100644
--- a/addons/sale_stock/__manifest__.py
+++ b/addons/sale_stock/__manifest__.py
@@ -35,6 +35,7 @@ Preferences
'wizard/stock_rules_report_views.xml',
],
'demo': ['data/sale_order_demo.xml'],
+ 'qweb': ['static/src/xml/qty_at_date.xml'],
'installable': True,
'auto_install': True,
}
diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py
index cbdb88a10a9..46cf1e16eb7 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
+from collections import defaultdict
from odoo import api, fields, models, _
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare
@@ -192,6 +193,92 @@ class SaleOrderLine(models.Model):
product_packaging = fields.Many2one('product.packaging', string='Package', default=False)
route_id = fields.Many2one('stock.location.route', string='Route', domain=[('sale_selectable', '=', True)], ondelete='restrict')
move_ids = fields.One2many('stock.move', 'sale_line_id', string='Stock Moves')
+ product_type = fields.Selection(related='product_id.type')
+ virtual_available_at_date = fields.Float(compute='_compute_qty_at_date')
+ scheduled_date = fields.Datetime(compute='_compute_qty_at_date')
+ free_qty_today = fields.Float(compute='_compute_qty_at_date')
+ qty_available_today = fields.Float(compute='_compute_qty_at_date')
+ warehouse_id = fields.Many2one(related='order_id.warehouse_id')
+ qty_to_deliver = fields.Float(compute='_compute_qty_to_deliver')
+ is_mto = fields.Boolean(compute='_compute_is_mto')
+ display_qty_widget = fields.Boolean(compute='_compute_qty_to_deliver')
+
+ @api.depends('product_id', 'product_uom_qty', 'qty_delivered', 'state')
+ def _compute_qty_to_deliver(self):
+ """Compute the visibility of the inventory widget."""
+ for line in self:
+ line.qty_to_deliver = line.product_uom_qty - line.qty_delivered
+ if line.state == 'draft' and line.product_type == 'product' and line.qty_to_deliver > 0:
+ line.display_qty_widget = True
+ else:
+ line.display_qty_widget = False
+
+ @api.depends('product_id', 'customer_lead', 'product_uom_qty', 'order_id.warehouse_id', 'order_id.commitment_date')
+ def _compute_qty_at_date(self):
+ """ Compute the quantity forecasted of product at delivery date. There are
+ two cases:
+ 1. The quotation has a commitment_date, we take it as delivery date
+ 2. The quotation hasn't commitment_date, we compute the estimated delivery
+ date based on lead time"""
+ qty_processed_per_product = defaultdict(lambda: 0)
+ grouped_lines = defaultdict(lambda: self.env['sale.order.line'])
+ # We first loop over the SO lines to group them by warehouse and schedule
+ # date in order to batch the read of the quantities computed field.
+ for line in self:
+ if not line.display_qty_widget:
+ continue
+ warehouse = line.order_id.warehouse_id
+ if line.order_id.commitment_date:
+ date = line.order_id.commitment_date
+ else:
+ confirm_date = line.order_id.confirmation_date if line.order_id.state in ['sale', 'done'] else datetime.now()
+ date = confirm_date + timedelta(days=line.customer_lead or 0.0)
+ grouped_lines[(warehouse.id, date)] |= line
+
+ for (warehouse, scheduled_date), lines in grouped_lines.items():
+ product_qties = lines.mapped('product_id').with_context(to_date=scheduled_date, warehouse=warehouse).read([
+ 'qty_available',
+ 'free_qty',
+ 'virtual_available',
+ ])
+ qties_per_product = {
+ product['id']: (product['qty_available'], product['free_qty'], product['virtual_available'])
+ for product in product_qties
+ }
+ for line in lines:
+ line.scheduled_date = scheduled_date
+ qty_available_today, free_qty_today, virtual_available_at_date = qties_per_product[line.product_id.id]
+ line.qty_available_today = qty_available_today - qty_processed_per_product[line.product_id.id]
+ line.free_qty_today = free_qty_today - qty_processed_per_product[line.product_id.id]
+ line.virtual_available_at_date = virtual_available_at_date - qty_processed_per_product[line.product_id.id]
+ qty_processed_per_product[line.product_id.id] += line.product_uom_qty
+
+ @api.depends('product_id', 'route_id', 'order_id.warehouse_id')
+ def _compute_is_mto(self):
+ """ Verify the route of the product based on the warehouse
+ set 'is_available' at True if the product availibility in stock does
+ not need to be verified, which is the case in MTO, Cross-Dock or Drop-Shipping
+ """
+ for line in self:
+ if not line.display_qty_widget:
+ continue
+ product = line.product_id
+ line.is_mto = False
+ product_routes = line.route_id or (product.route_ids + product.categ_id.total_route_ids)
+
+ # Check MTO
+ wh_mto_route = line.order_id.warehouse_id.mto_pull_id.route_id
+ if wh_mto_route and wh_mto_route.mapped('sequence') <= product_routes.mapped('sequence'):
+ line.is_mto = True
+ else:
+ mto_route = False
+ try:
+ mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto', _('Make To Order'))
+ except UserError:
+ # if route MTO not found in ir_model_data, we treat the product as in MTS
+ pass
+ if mto_route and mto_route in product_routes:
+ line.is_mto = True
@api.depends('product_id')
def _compute_qty_delivered_method(self):
@@ -262,56 +349,6 @@ class SaleOrderLine(models.Model):
if self.product_packaging:
return self._check_package()
- @api.onchange('product_id')
- def _onchange_product_id_uom_check_availability(self):
- if not self.product_uom or (self.product_id.uom_id.category_id.id != self.product_uom.category_id.id):
- self.product_uom = self.product_id.uom_id
- self._onchange_product_id_check_availability()
-
- @api.onchange('product_uom_qty', 'product_uom', 'route_id')
- def _onchange_product_id_check_availability(self):
- if not self.product_id or not self.product_uom_qty or not self.product_uom:
- self.product_packaging = False
- return {}
- return self._check_availability(self.product_id)
-
- def _check_availability(self, product_id):
- """ The purpose of this method is only to be overridden 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_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'
- )
- 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, 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 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') % \
- (product_id.virtual_available, product_by_wh.uom_id.name)
- for warehouse in self.env['stock.warehouse'].search([]):
- quantity = product_id.with_context(warehouse=warehouse.id).virtual_available
- if quantity > 0:
- message += "%s: %s %s\n" % (warehouse.name, quantity, product_id.uom_id.name)
- warning_mess = {
- 'title': _('Not enough inventory!'),
- 'message': message
- }
- return {'warning': warning_mess}
- return {}
-
@api.onchange('product_uom_qty')
def _onchange_product_uom_qty(self):
# When modifying a one2many, _origin doesn't guarantee that its values will be the ones
@@ -450,38 +487,6 @@ class SaleOrderLine(models.Model):
}
return {}
- 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 (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
- if wh_mto_route and wh_mto_route <= product_routes:
- is_available = True
- else:
- mto_route = False
- try:
- mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto', _('Make To Order'))
- except UserError:
- # if route MTO not found in ir_model_data, we treat the product as in MTS
- pass
- if mto_route and mto_route in product_routes:
- is_available = True
-
- # Check Drop-Shipping
- if not is_available:
- for pull_rule in product_routes.mapped('rule_ids'):
- if pull_rule.picking_type_id.sudo().default_location_src_id.usage == 'supplier' and\
- pull_rule.picking_type_id.sudo().default_location_dest_id.usage == 'customer':
- is_available = True
- break
-
- return is_available
-
def _update_line_quantity(self, values):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
line_products = self.filtered(lambda l: l.product_id.type in ['product', 'consu'])
diff --git a/addons/sale_stock/static/src/js/qty_at_date_widget.js b/addons/sale_stock/static/src/js/qty_at_date_widget.js
new file mode 100644
index 00000000000..f19403f4f3b
--- /dev/null
+++ b/addons/sale_stock/static/src/js/qty_at_date_widget.js
@@ -0,0 +1,90 @@
+odoo.define('sale_stock.QtyAtDateWidget', function (require) {
+"use strict";
+
+var core = require('web.core');
+var QWeb = core.qweb;
+
+var Widget = require('web.Widget');
+var Context = require('web.Context');
+var Dialog = require('web.Dialog');
+var data_manager = require('web.data_manager');
+var widget_registry = require('web.widget_registry');
+
+var _t = core._t;
+var time = require('web.time');
+
+var QtyAtDateWidget = Widget.extend({
+ template: 'sale_stock.qtyAtDate',
+ events: _.extend({}, Widget.prototype.events, {
+ 'click .fa-info-circle': '_onClickButton',
+ }),
+
+ /**
+ * @override
+ * @param {Widget|null} parent
+ * @param {Object} params
+ */
+ init: function (parent, params) {
+ this.data = params.data;
+ this._super(parent);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+ _onClickButton: function (ev) {
+ ev.stopPropagation();
+ var self = this;
+ this.data.delivery_date = this.data.scheduled_date.clone().add(this.getSession().getTZOffset(this.data.scheduled_date), 'minutes').format(time.getLangDateFormat());
+ // The grid view need a specific date format that could be different than
+ // the user one.
+ this.data.delivery_date_grid = this.data.scheduled_date.clone().add(this.getSession().getTZOffset(this.data.scheduled_date), 'minutes').format('YYYY-MM-DD');
+ var $content = $(QWeb.render('sale_stock.QtyDetailDialog', {
+ data: this.data,
+ }));
+ this.dialog = new Dialog(this, {
+ size: 'medium',
+ title: _.str.sprintf(_t('%s Availability'), self.data.product_id.data.display_name),
+ $content: $content,
+ buttons: [{
+ text: _t('Close'),
+ classes: 'btn-primary',
+ close: true,
+ }],
+ });
+ this.dialog.opened().then(function () {
+ var $forecastButton = self.dialog.$('.action_open_forecast');
+ $forecastButton.on('click', function(ev) {
+ ev.stopPropagation();
+ data_manager.load_action('stock.report_stock_quantity_action').then(function (action) {
+ // Change action context to choose a specific date and product(s)
+ // As grid_anchor is set to now() by default in the data, we need
+ // to load the action first, change the context then launch it via do_action
+ // additional_context cannot replace a context value, only add new
+ //
+ // in case of kit product, the forecast view show the kit's components
+ self._rpc({
+ model: 'product.product',
+ method: 'get_components',
+ args: [[self.data.product_id.data.id]]
+ }).then(function (res) {
+ var additional_context = {};
+ additional_context.grid_anchor = self.data.delivery_date_grid;
+ additional_context.search_default_warehouse_id = [self.data.warehouse_id.data.id]
+ action.context = new Context(action.context, additional_context);
+ action.domain = [
+ ['product_id', 'in', res]
+ ];
+ self.do_action(action);
+ });
+ });
+ });
+ });
+ this.dialog.open();
+ },
+});
+
+widget_registry.add('qty_at_date_widget', QtyAtDateWidget);
+
+return QtyAtDateWidget;
+});
diff --git a/addons/sale_stock/static/src/xml/qty_at_date.xml b/addons/sale_stock/static/src/xml/qty_at_date.xml
new file mode 100644
index 00000000000..b0cd34626f1
--- /dev/null
+++ b/addons/sale_stock/static/src/xml/qty_at_date.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Delivery Date
+
+
+
+
Forecasted Stock
On delivery date
+
+
+
+
+
+
+
+
+
+
+
+
Stock On Hand
+
+
+
+
+
Stock Available
On Hand - Reserved
+
+
+
+
+
+
+
This product is replenished on demand.
+
+
+
+
+
+
diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py
index 66b61fe8c97..9fd480aa392 100644
--- a/addons/sale_stock/tests/test_sale_stock.py
+++ b/addons/sale_stock/tests/test_sale_stock.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime, timedelta
from odoo.addons.sale.tests.test_sale_common import TestSale
from odoo.exceptions import UserError
@@ -559,3 +560,49 @@ class TestSaleStock(TestSale):
self.assertEqual(so1.order_line.qty_delivered, 5)
self.assertEqual(so1.picking_ids[-1].move_lines.product_qty, 10)
+ def test_09_qty_available(self):
+ """ create a sale order in warehouse1, change to warehouse2 and check the
+ available quantities on sale order lines are well updated """
+ # sell two products
+ item1 = self.products['prod_order']
+ item1.type = 'product'
+ warehouse1 = self.env.ref('stock.warehouse0')
+ self.env['stock.quant']._update_available_quantity(item1, warehouse1.lot_stock_id, 10)
+ self.env['stock.quant']._update_reserved_quantity(item1, warehouse1.lot_stock_id, 3)
+ warehouse2 = self.env['stock.warehouse'].search([('id', '!=', warehouse1.id)], limit=1)
+ self.env['stock.quant']._update_available_quantity(item1, warehouse2.lot_stock_id, 5)
+ so = self.env['sale.order'].create({
+ 'partner_id': self.partner.id,
+ 'order_line': [
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 1, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ ],
+ })
+ line = so.order_line[0]
+ self.assertAlmostEqual(line.scheduled_date, datetime.now(), delta=timedelta(seconds=10))
+ self.assertEqual(line.virtual_available_at_date, 10)
+ self.assertEqual(line.free_qty_today, 7)
+ self.assertEqual(line.qty_available_today, 10)
+ self.assertEqual(line.warehouse_id, warehouse1)
+ self.assertEqual(line.qty_to_deliver, 1)
+ so.warehouse_id = warehouse2
+ self.assertEqual(line.virtual_available_at_date, 5)
+ self.assertEqual(line.free_qty_today, 5)
+ self.assertEqual(line.qty_available_today, 5)
+ self.assertEqual(line.warehouse_id, warehouse2)
+ self.assertEqual(line.qty_to_deliver, 1)
+
+ def test_10_qty_available(self):
+ """create a sale order containing three times the same product. The
+ quantity available should be different for the 3 lines"""
+ item1 = self.products['prod_order']
+ item1.type = 'product'
+ self.env['stock.quant']._update_available_quantity(item1, self.env.ref('stock.stock_location_stock'), 10)
+ so = self.env['sale.order'].create({
+ 'partner_id': self.partner.id,
+ 'order_line': [
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 5, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 5, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 5, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ ],
+ })
+ self.assertEqual(so.order_line.mapped('free_qty_today'), [10, 5, 0])
diff --git a/addons/sale_stock/views/sale_order_views.xml b/addons/sale_stock/views/sale_order_views.xml
index f328b550e3a..fc7173c469d 100644
--- a/addons/sale_stock/views/sale_order_views.xml
+++ b/addons/sale_stock/views/sale_order_views.xml
@@ -75,5 +75,32 @@
+
+
+ sale.order.line.tree.sale.stock.qty
+
+ sale.order
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index 1e230033233..638086e96ca 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -189,6 +189,10 @@ class Product(models.Model):
return res
+ def get_components(self):
+ self.ensure_one()
+ return self.ids
+
def _get_description(self, picking_type_id):
""" return product receipt/delivery/picking description depending on
picking type passed as argument.
diff --git a/addons/stock/report/report_stock_quantity.xml b/addons/stock/report/report_stock_quantity.xml
index a0987a1ca94..39a030e3e14 100644
--- a/addons/stock/report/report_stock_quantity.xml
+++ b/addons/stock/report/report_stock_quantity.xml
@@ -20,6 +20,7 @@
+
diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py
index 3f136c605d7..53c471e19f4 100644
--- a/addons/stock_dropshipping/models/sale.py
+++ b/addons/stock_dropshipping/models/sale.py
@@ -19,3 +19,15 @@ class SaleOrderLine(models.Model):
return qty
else:
return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty)
+
+ def _compute_is_mto(self):
+ super(SaleOrderLine, self)._compute_is_mto()
+ for line in self:
+ if not line.display_qty_widget or line.is_mto:
+ continue
+ product_routes = line.route_id or (line.product_id.route_ids + line.product_id.categ_id.total_route_ids)
+ for pull_rule in product_routes.mapped('rule_ids'):
+ if pull_rule.picking_type_id.sudo().default_location_src_id.usage == 'supplier' and\
+ pull_rule.picking_type_id.sudo().default_location_dest_id.usage == 'customer':
+ line.is_mto = True
+ break