[ADD] stock: report forecast quantity

Create a new report in order to see quantity in the past
and in the future. Also display the planned in and out moves
at their schedule date. The view available for this report is
a graph view even if it could be display with other views.

Technically it's an SQL views based on stock.quant and stock.move
Currently the interval for the report is -3 months, +3 months because
the forecast quantity is based on quants - moves during this interval
in order to keep a linear performance that's not growing with the data.

task: 2000948

closes odoo/odoo#34898

Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
Arnold Moyaux
2019-07-16 15:20:49 +00:00
parent dc35b88411
commit b311d9ccba
7 changed files with 197 additions and 0 deletions
+1
View File
@@ -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',
+1
View File
@@ -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
@@ -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)
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="stock_report_view_graph" model="ir.ui.view">
<field name="name">stock_report_view_graph</field>
<field name="model">report.stock.quantity</field>
<field name="arch" type="xml">
<graph string="report_stock_quantity_graph" type="line">
<field name="date" interval="day"/>
<field name="product_id" type="row"/>
<field name="product_qty" type="measure"/>
</graph>
</field>
</record>
<record id="stock_report_view_search" model="ir.ui.view">
<field name="name">report.stock.quantity.search</field>
<field name="model">report.stock.quantity</field>
<field name="arch" type="xml">
<search string="Report Quantity">
<field name="product_id"/>
<field name="state"/>
<field name="product_tmpl_id"/>
<group expand="0" string="State">
<filter string="Forecasted Stock" name="filter_forecast" domain="[('state', '=', 'forecast')]"/>
<filter string="Forecasted Receipts" name="filter_in" domain="[('state', '=', 'in')]"/>
<filter string="Forecasted Deliveries" name="filter_out" domain="[('state', '=', 'out')]"/>
</group>
<group expand="0" string="Group By">
<filter string="Date" name="groupby_date" domain="[]" context="{'group_by':'date:day'}"/>
</group>
</search>
</field>
</record>
<record id="report_stock_quantity_action_product" model="ir.actions.act_window">
<field name="name">Forecasted Inventory</field>
<field name="res_model">report.stock.quantity</field>
<field name="view_mode">graph</field>
<field name="context">{
'search_default_filter_forecast': 1,
'graph_groupbys': ['date:day', 'state'],
}</field>
</record>
<record id="report_stock_quantity_action" model="ir.actions.act_window">
<field name="name">Forecasted Inventory</field>
<field name="res_model">report.stock.quantity</field>
<field name="view_mode">graph</field>
<field name="context">{
'search_default_filter_forecast': 1,
'graph_groupbys': ['date:day', 'state', 'product_id'],
}</field>
</record>
</odoo>
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
70 access_product_product_attribute_manager product.product.attribute manager value product.model_product_template_attribute_value stock.group_stock_manager 1 1 1 1
71 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
72 access_product_template_attribute_line_manager product.attribute manager line product.model_product_template_attribute_line stock.group_stock_manager 1 1 1 1
73 access_report_stock_quantity access_report_stock_quantity model_report_stock_quantity base.group_user 1 0 0 0
+7
View File
@@ -172,5 +172,12 @@
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="report_stock_quantity_flow_comp_rule">
<field name="name">report_stock_quantity_flow multi-company</field>
<field name="model_id" ref="model_report_stock_quantity"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
</data>
</odoo>
+33
View File
@@ -224,6 +224,22 @@
<span class="o_stat_text">On Hand</span>
</div>
</button>
<button type="action"
name="%(stock.report_stock_quantity_action_product)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
context="{'search_default_product_id': id}"
class="oe_stat_button" icon="fa-building-o">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="virtual_available" widget="statinfo" nolabel="1"/>
<span attrs="{'invisible': [('outgoing_qty', '=', 0), ('incoming_qty', '=', 0)]}" groups="base.group_no_one">
(-<field name="outgoing_qty" widget="statinfo" nolabel="1"/>+<field name="incoming_qty" widget="statinfo" nolabel="1"/>)
</span>
<field name="uom_name" class="ml4"/>
</span>
<span class="o_stat_text">Forecasted</span>
</div>
</button>
<button string="Product Moves"
type="object"
name= "action_view_stock_move_lines"
@@ -302,6 +318,22 @@
<span class="o_stat_text">On Hand</span>
</div>
</button>
<button type="action"
name="%(stock.report_stock_quantity_action_product)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
context="{'search_default_product_tmpl_id': id}"
class="oe_stat_button" icon="fa-building-o">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="virtual_available" widget="statinfo" nolabel="1"/>
<span attrs="{'invisible':[('outgoing_qty', '=', 0),('incoming_qty','=',0)]}" groups="base.group_no_one">
(-<field name="outgoing_qty" widget="statinfo" nolabel="1"/>+<field name="incoming_qty" widget="statinfo" nolabel="1"/>)
</span>
<field name="uom_name" class="ml4"/>
</span>
<span class="o_stat_text">Forecasted</span>
</div>
</button>
<button string="Product Moves" type="object"
name= "action_view_stock_move_lines"
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
@@ -388,6 +420,7 @@
<menuitem id="product_product_menu" name="Product Variants" action="stock_product_normal_action"
parent="menu_stock_inventory_control" sequence="2" groups="product.group_product_variant"/>
<menuitem id="menu_product_packagings" name="Product Packagings" parent="stock.menu_product_in_config_stock" action="product.action_packaging_view" groups="product.group_stock_packaging"/>
<menuitem id="menu_forecast_inventory" name="Forecasted Inventory" parent="stock.menu_warehouse_report" sequence="101" action="stock.report_stock_quantity_action"/>
</data>
</odoo>