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