diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py
index 217bec03858..7df37828b1e 100644
--- a/addons/stock/__manifest__.py
+++ b/addons/stock/__manifest__.py
@@ -26,6 +26,7 @@
'data/stock_traceability_report_data.xml',
'data/procurement_data.xml',
+ 'report/report_stock_quantity.xml',
'report/stock_report_views.xml',
'report/report_package_barcode.xml',
'report/report_lot_barcode.xml',
diff --git a/addons/stock/report/__init__.py b/addons/stock/report/__init__.py
index 55ce8712c5c..437a4c100c9 100644
--- a/addons/stock/report/__init__.py
+++ b/addons/stock/report/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import report_stock_quantity
from . import report_stock_rule
diff --git a/addons/stock/report/report_stock_quantity.py b/addons/stock/report/report_stock_quantity.py
new file mode 100644
index 00000000000..d06a1a96b4d
--- /dev/null
+++ b/addons/stock/report/report_stock_quantity.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, tools
+
+
+class ReportStockQuantity(models.Model):
+ _name = 'report.stock.quantity'
+ _auto = False
+ _description = 'Stock Quantity Report'
+
+ date = fields.Date(string='Date', readonly=True)
+ product_tmpl_id = fields.Many2one('product.template', related='product_id.product_tmpl_id')
+ product_id = fields.Many2one('product.product', string='Product', readonly=True)
+ state = fields.Selection([
+ ('forecast', 'Forecasted Stock'),
+ ('in', 'Forecasted Receipts'),
+ ('out', 'Forecasted Deliveries'),
+ ], string='State', readonly=True)
+ product_qty = fields.Float(string='Quantity', readonly=True)
+ move_ids = fields.One2many('stock.move', readonly=True)
+ company_id = fields.Many2one('res.company', readonly=True)
+
+ def init(self):
+ tools.drop_view_if_exists(self._cr, 'report_stock_quantity')
+ query = """
+CREATE or REPLACE VIEW report_stock_quantity AS (
+SELECT
+ m.id,
+ product_id,
+ CASE
+ WHEN (ls.usage = 'internal' OR ls.usage = 'transit' AND ls.company_id IS NOT NULL) AND ld.usage != 'internal' THEN 'out'
+ WHEN ls.usage != 'internal' AND (ld.usage = 'internal' OR ld.usage = 'transit' AND ld.company_id IS NOT NULL) THEN 'in'
+ END AS state,
+ date_expected::date AS date,
+ CASE
+ WHEN (ls.usage = 'internal' OR ls.usage = 'transit' AND ls.company_id IS NOT NULL) AND ld.usage != 'internal' THEN -product_qty
+ WHEN ls.usage != 'internal' AND (ld.usage = 'internal' OR ld.usage = 'transit' AND ld.company_id IS NOT NULL) THEN product_qty
+ END AS product_qty,
+ m.company_id
+FROM
+ stock_move m
+LEFT JOIN stock_location ls on (ls.id=m.location_id)
+LEFT JOIN stock_location ld on (ld.id=m.location_dest_id)
+WHERE
+ product_qty != 0 AND (
+ (ls.usage = 'internal' OR ls.usage = 'transit' AND ls.company_id IS NOT NULL) AND ld.usage != 'internal' OR
+ ls.usage != 'internal' AND (ld.usage = 'internal' OR ld.usage = 'transit' AND ld.company_id IS NOT NULL)) AND
+ m.state NOT IN ('cancel', 'draft', 'done')
+UNION
+SELECT
+ -MIN(q.id) as id,
+ product_id,
+ 'forecast' as state,
+ date.*::date,
+ SUM(quantity) as product_qty,
+ q.company_id
+FROM
+ GENERATE_SERIES((now() at time zone 'utc')::date - interval '3month',
+ (now() at time zone 'utc')::date + interval '3 month', '1 day'::interval) date,
+ stock_quant q
+LEFT JOIN stock_location l on (l.id=q.location_id)
+WHERE
+ l.usage = 'internal'
+GROUP BY
+ product_id, date, q.company_id
+UNION
+SELECT
+ m.id,
+ product_id,
+ 'forecast' as state,
+ GENERATE_SERIES(
+ CASE
+ WHEN state = 'done' THEN (now() at time zone 'utc')::date - interval '3month'
+ ELSE date_expected::date
+ END,
+ CASE
+ WHEN state != 'done' THEN (now() at time zone 'utc')::date + interval '3 month'
+ ELSE date::date - interval '1 day'
+ END, '1 day'::interval)::date date,
+ CASE
+ WHEN (ls.usage = 'internal' OR ls.usage = 'transit' AND ls.company_id IS NOT NULL) AND ld.usage != 'internal' AND state = 'done' THEN product_qty
+ WHEN ls.usage != 'internal' AND (ld.usage = 'internal' OR ld.usage = 'transit' AND ld.company_id IS NOT NULL) AND state = 'done' THEN -product_qty
+ WHEN (ls.usage = 'internal' OR ls.usage = 'transit' AND ls.company_id IS NOT NULL) AND ld.usage != 'internal' THEN -product_qty
+ WHEN ls.usage != 'internal' AND (ld.usage = 'internal' OR ld.usage = 'transit' AND ld.company_id IS NOT NULL) THEN product_qty
+ END AS product_qty,
+ m.company_id
+FROM
+ stock_move m
+LEFT JOIN stock_location ls on (ls.id=m.location_id)
+LEFT JOIN stock_location ld on (ld.id=m.location_dest_id)
+WHERE
+ product_qty != 0 AND (
+ (ls.usage = 'internal' OR ls.usage = 'transit' AND ls.company_id IS NOT NULL) AND ld.usage != 'internal' OR
+ ls.usage != 'internal' AND (ld.usage = 'internal' OR ld.usage = 'transit' AND ld.company_id IS NOT NULL)) AND
+ m.state NOT IN ('cancel', 'draft')
+);
+"""
+ self.env.cr.execute(query)
diff --git a/addons/stock/report/report_stock_quantity.xml b/addons/stock/report/report_stock_quantity.xml
new file mode 100644
index 00000000000..a0987a1ca94
--- /dev/null
+++ b/addons/stock/report/report_stock_quantity.xml
@@ -0,0 +1,55 @@
+
+
+
+ stock_report_view_graph
+ report.stock.quantity
+
+
+
+
+
+
+
+
+
+
+ report.stock.quantity.search
+ report.stock.quantity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Forecasted Inventory
+ report.stock.quantity
+ graph
+ {
+ 'search_default_filter_forecast': 1,
+ 'graph_groupbys': ['date:day', 'state'],
+ }
+
+
+
+ Forecasted Inventory
+ report.stock.quantity
+ graph
+ {
+ 'search_default_filter_forecast': 1,
+ 'graph_groupbys': ['date:day', 'state', 'product_id'],
+ }
+
+
+
diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv
index b0eb20c8afc..599da9f91c9 100644
--- a/addons/stock/security/ir.model.access.csv
+++ b/addons/stock/security/ir.model.access.csv
@@ -70,3 +70,4 @@ access_product_attribute_value_manager,product.attribute manager value,product.m
access_product_product_attribute_manager,product.product.attribute manager value,product.model_product_template_attribute_value,stock.group_stock_manager,1,1,1,1
access_product_template_attribute_exclusion_manager,product.attribute manager filter line,product.model_product_template_attribute_exclusion,stock.group_stock_manager,1,1,1,1
access_product_template_attribute_line_manager,product.attribute manager line,product.model_product_template_attribute_line,stock.group_stock_manager,1,1,1,1
+access_report_stock_quantity,access_report_stock_quantity,model_report_stock_quantity,base.group_user,1,0,0,0
diff --git a/addons/stock/security/stock_security.xml b/addons/stock/security/stock_security.xml
index 0adb7c6db34..2ce2d742bdf 100644
--- a/addons/stock/security/stock_security.xml
+++ b/addons/stock/security/stock_security.xml
@@ -172,5 +172,12 @@
[('company_id', 'in', company_ids)]
+
+ report_stock_quantity_flow multi-company
+
+
+ ['|',('company_id','=',False),('company_id', 'in', company_ids)]
+
+
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index ae2c14b4714..660897759fc 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -224,6 +224,22 @@
On Hand
+
+
+