[ADD] stock: Report Forecasted Inventory

Purpose of this task:
Show forecasted inventory for all products.
Before this commit we have to search forecasted inventory for each product individually.
There is no way to check all the forecasted inventory in a single menu.

After this commit:
User can search for all forecasted inventory from the menu 'Forecasted Inventory'.
Features added.
1. Menu for Forecasted Inventory has been added.
2. Default measures has been set to 'Quantity' and 'Cumulative Quantity'.
3. Pivot view is grouped by product (vertical) and by weeks (horizontal).

Why query changes required:
User can search for all forecasted inventory from the menu 'Forecasted Inventory'.
Features added.
1. lines only those containing any stock move will be shown.
2. Reference field added.
3. We remove 'week' from the query, instead of we fetch all the data according to 'date_expected',
and we create three group_bys (i.e. group by date, week and month)
so it is easy for the user to check report according to day, week and month.

This commit is related to task 36088.
This commit is contained in:
Hetashree Chauhan
2019-02-12 12:12:14 +00:00
committed by Arnold Moyaux
parent 7e49fda883
commit d3f14f6a1d
3 changed files with 194 additions and 108 deletions
+165 -91
View File
@@ -14,102 +14,176 @@ class ReportStockForecat(models.Model):
cumulative_quantity = fields.Float(string='Cumulative Quantity', readonly=True)
quantity = fields.Float(readonly=True)
company_id = fields.Many2one('res.company', string='Company', readonly=True)
reference = fields.Char('Reference')
@api.model_cr
def init(self):
tools.drop_view_if_exists(self._cr, 'report_stock_forecast')
self._cr.execute("""CREATE or REPLACE VIEW report_stock_forecast AS (SELECT
MIN(id) as id,
product_id as product_id,
date as date,
sum(product_qty) AS quantity,
sum(sum(product_qty)) OVER (PARTITION BY product_id ORDER BY date) AS cumulative_quantity,
company_id
row_number() OVER (ORDER BY final.date,final.id) AS id,
product_id as product_id,
date(final.date) as date,
sum(product_qty) AS quantity,
sum(sum(product_qty)) OVER (PARTITION BY final.product_id, final.company_id ORDER BY final.date) AS cumulative_quantity,
reference,
final.company_id
FROM
(SELECT
MIN(id) as id,
MAIN.product_id as product_id,
SUB.date as date,
CASE WHEN MAIN.date = SUB.date THEN sum(MAIN.product_qty) ELSE 0 END as product_qty,
MAIN.company_id as company_id
FROM
(SELECT
MIN(sq.id) as id,
sq.product_id,
date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) as date,
SUM(sq.quantity) AS product_qty,
sq.company_id
(SELECT
MIN(id) as id,
MAIN.product_id as product_id,
date(SUB.date) as date,
CASE WHEN MAIN.date = SUB.date THEN sum(MAIN.product_qty) ELSE 0 END as product_qty,
MAIN.reference as reference,
MAIN.company_id as company_id
FROM
stock_quant as sq
(SELECT
MIN(-product.id) as id,
product.id as product_id,
date(CURRENT_DATE) as date,
SUM(sq.quantity) AS product_qty,
'Starting Inventory' as reference,
sq.company_id
FROM
product_product as product
LEFT JOIN
stock_quant sq ON product.id = sq.product_id
LEFT JOIN
stock_location location_id ON sq.location_id = location_id.id
WHERE
location_id.usage = 'internal'
GROUP BY
date, product.id, sq.company_id, sq.quantity
UNION ALL
SELECT
MIN(sm.id) as id,
sm.product_id,
CASE WHEN sm.date_expected > CURRENT_DATE
THEN date(sm.date_expected)
ELSE date(CURRENT_DATE) END
AS date,
SUM(sm.product_qty) AS product_qty,
sm.reference as reference,
sm.company_id
FROM
stock_move as sm
LEFT JOIN
product_product ON product_product.id = sm.product_id
LEFT JOIN
stock_location dest_location ON sm.location_dest_id = dest_location.id
LEFT JOIN
stock_location source_location ON sm.location_id = source_location.id
WHERE
sm.state IN ('confirmed','partially_available','assigned','waiting') and
source_location.usage != 'internal' and dest_location.usage = 'internal'
GROUP BY
sm.date_expected,sm.product_id, sm.company_id, sm.reference
UNION ALL
SELECT
MIN(sm.id) as id,
sm.product_id,
CASE WHEN sm.date_expected > CURRENT_DATE
THEN date(sm.date_expected)
ELSE date(CURRENT_DATE) END
AS date,
SUM(-(sm.product_qty)) AS product_qty,
sm.reference AS reference,
sm.company_id
FROM
stock_move as sm
LEFT JOIN
product_product ON product_product.id = sm.product_id
LEFT JOIN
stock_location source_location ON sm.location_id = source_location.id
LEFT JOIN
stock_location dest_location ON sm.location_dest_id = dest_location.id
WHERE
sm.state IN ('confirmed','partially_available','assigned','waiting') and
source_location.usage = 'internal' and dest_location.usage != 'internal'
GROUP BY
sm.date_expected,sm.product_id, sm.company_id, sm.reference
) AS MAIN
LEFT JOIN
product_product ON product_product.id = sq.product_id
LEFT JOIN
stock_location location_id ON sq.location_id = location_id.id
WHERE
location_id.usage = 'internal'
GROUP BY date, sq.product_id, sq.company_id
UNION ALL
SELECT
MIN(-sm.id) as id,
sm.product_id,
CASE WHEN sm.date_expected > CURRENT_DATE
THEN date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD'))
ELSE date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) END
AS date,
SUM(sm.product_qty) AS product_qty,
sm.company_id
FROM
stock_move as sm
LEFT JOIN
product_product ON product_product.id = sm.product_id
LEFT JOIN
stock_location dest_location ON sm.location_dest_id = dest_location.id
LEFT JOIN
stock_location source_location ON sm.location_id = source_location.id
WHERE
sm.state IN ('confirmed','partially_available','assigned','waiting') and
source_location.usage != 'internal' and dest_location.usage = 'internal'
GROUP BY sm.date_expected,sm.product_id, sm.company_id
UNION ALL
SELECT
MIN(-sm.id) as id,
sm.product_id,
CASE WHEN sm.date_expected > CURRENT_DATE
THEN date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD'))
ELSE date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) END
AS date,
SUM(-(sm.product_qty)) AS product_qty,
sm.company_id
FROM
stock_move as sm
LEFT JOIN
product_product ON product_product.id = sm.product_id
LEFT JOIN
stock_location source_location ON sm.location_id = source_location.id
LEFT JOIN
stock_location dest_location ON sm.location_dest_id = dest_location.id
WHERE
sm.state IN ('confirmed','partially_available','assigned','waiting') and
source_location.usage = 'internal' and dest_location.usage != 'internal'
GROUP BY sm.date_expected,sm.product_id, sm.company_id)
as MAIN
LEFT JOIN
(SELECT DISTINCT date
FROM
(
SELECT date_trunc('week', CURRENT_DATE) AS DATE
UNION ALL
SELECT date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD')) AS date
FROM stock_move sm
LEFT JOIN
stock_location source_location ON sm.location_id = source_location.id
LEFT JOIN
stock_location dest_location ON sm.location_dest_id = dest_location.id
WHERE
sm.state IN ('confirmed','assigned','waiting') and sm.date_expected > CURRENT_DATE and
((dest_location.usage = 'internal' AND source_location.usage != 'internal')
or (source_location.usage = 'internal' AND dest_location.usage != 'internal'))) AS DATE_SEARCH)
SUB ON (SUB.date IS NOT NULL)
GROUP BY MAIN.product_id,SUB.date, MAIN.date, MAIN.company_id
) AS FINAL
GROUP BY product_id,date,company_id)""")
(SELECT
DISTINCT date
FROM
(SELECT
date_trunc('day', CURRENT_DATE) AS DATE
UNION ALL
SELECT
date(sm.date_expected) AS date
FROM
stock_move sm
LEFT JOIN
stock_location source_location ON sm.location_id = source_location.id
LEFT JOIN
stock_location dest_location ON sm.location_dest_id = dest_location.id
WHERE
sm.state IN ('confirmed','assigned','waiting') AND
sm.date_expected > CURRENT_DATE AND
((dest_location.usage = 'internal' AND
source_location.usage != 'internal') OR
(source_location.usage = 'internal' AND
dest_location.usage != 'internal'))
) AS DATE_SEARCH
) AS SUB ON SUB.date IS NOT NULL
GROUP BY
MAIN.product_id,SUB.date, MAIN.date, MAIN.company_id,MAIN.reference
) AS FINAL
LEFT JOIN
stock_picking sp ON final.reference=sp.name
WHERE
final.product_qty != 0 OR final.reference = 'Starting Inventory'
GROUP BY
product_id,final.date,final.id,final.company_id,final.reference,sp.priority,sp.date,sp.id)""")
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
""" Override read_group in order to display the correct cumulative
quantity. The cumulative quantity for a group of moves/quants
should be the last for the selected time interval (given by domain).
However select the 'last' record's value in a groupby is not possible
(only possible aggregation functions are the ones provided by
PostgreSQL). The cumulative quantity is recomputed since a search on
each group returned by the super would consume too much time. The
cumulative quantity is computed based on previous group.
- Group1: sum of the quantity of records in group1.
- Group2: cumulative_quantity of group1 + sum of quantity of records in group2
- Group3: cumulative_quantity of group2 + sum of quantity of records in group3
...
Since read_group depends on domain, some records could be excluded and
the computed cumulative quantity could be different than the one computed
in SQL. It's wanted since the customer could play with the domain in
order to analyze different quantities in different situations.
"""
# Sort result by date_expected and id.
orderby = 'date, id' if not orderby else orderby
if 'cumulative_quantity' in fields and 'quantity' not in fields:
fields.append('quantity')
res = super(ReportStockForecat, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
total_cumulative_quantity = 0
cumulative_quantity_by_products = {}
if 'cumulative_quantity' in fields:
for index, line in enumerate(res):
# If the stock forecast cell have a 0 quantity the read_group
# will not return their sum as 0 but False.
if 'quantity' in line and not line['quantity']:
line['quantity'] = 0.0
if line.get('product_id'):
product_id = line['product_id']
if not cumulative_quantity_by_products.get(product_id):
cumulative_quantity_by_products[product_id] = 0
cumulative_quantity_by_products[product_id] += line['quantity'] # Get cumulative quantity product wise
line['cumulative_quantity'] = cumulative_quantity_by_products[product_id] # Sum of all quantities (i.e. cumulative quantity) product wise
elif line.get('reference'):
if line['reference'] == 'Starting Inventory':
if index == 0:
line['cumulative_quantity'] = line['quantity']
else:
line['quantity'] = 0.0
line['cumulative_quantity'] = res[index - 1]['cumulative_quantity'] # Set cumulatiove quantity if line does not have any cumulative
elif line.get('quantity', False):
total_cumulative_quantity += line['quantity']
line['cumulative_quantity'] = total_cumulative_quantity # Sum of all Move Operations' quantity
else:
line['cumulative_quantity'] = 0
return res
+26 -17
View File
@@ -7,19 +7,10 @@
<field name="arch" type="xml">
<pivot string="Stock Level forecast" disable_linking="True">
<field name="product_id" type="row"/>
<field name="date" type="col"/>
<field name="quantity" type="measure"/>
</pivot>
</field>
</record>
<record id="view_stock_level_forecast_graph" model="ir.ui.view">
<field name="name">Stock.forecast.graph</field>
<field name="model">report.stock.forecast</field>
<field name="arch" type="xml">
<graph string="Stock Level forecast" type="line">
<field name="product_id" type="col"/>
<field name="cumulative_quantity" type="measure"/>
</graph>
</pivot>
</field>
</record>
@@ -32,8 +23,10 @@
<field name="product_tmpl_id"/>
<field name="date"/>
<group expand="1" string="Group By">
<filter string="Pivot" name="pivot_by" context="{'pivot_row_groupby': ['product_id'], 'pivot_column_groupby': ['date:week'],'pivot_measures': ['quantity']}"/>
<filter string="Graph" name="graph_by" context="{'graph_groupbys': ['date:week', 'product_id']}"/>
<filter string="Product" name="inventory_by_product" context="{'group_by': 'product_id'}"/>
<filter string="Day" name="inventory_by_day" context="{'pivot_measures': ['quantity', 'cumulative_quantity'], 'pivot_column_groupby': ['date:day'], 'pivot_row_groupby': ['product_id']}"/>
<filter string="Week" name="inventory_by_week" context="{'pivot_measures': ['quantity', 'cumulative_quantity'], 'pivot_column_groupby': ['date:week'], 'pivot_row_groupby': ['product_id']}"/>
<filter string="Month" name="inventory_by_month" context="{'pivot_measures': ['quantity', 'cumulative_quantity'], 'pivot_column_groupby': ['date:month'], 'pivot_row_groupby': ['product_id']}"/>
</group>
</search>
</field>
@@ -43,9 +36,9 @@
<field name="name">Stock Level Forecast</field>
<field name="res_model">report.stock.forecast</field>
<field name="view_type">form</field>
<field name="view_mode">graph,pivot</field>
<field name="view_mode">pivot</field>
<field name="search_view_id" ref="view_stock_level_forecast_filter"/>
<field name="context">{'search_default_product_tmpl_id': active_id, 'group_by': ['company_id']}</field>
<field name="context">{'search_default_product_tmpl_id': active_id, 'search_default_inventory_by_week':1}</field>
<field name="view_id" ref="view_stock_level_forecast_pivot"/>
</record>
@@ -53,9 +46,25 @@
<field name="name">Stock Level Forecast</field>
<field name="res_model">report.stock.forecast</field>
<field name="view_type">form</field>
<field name="view_mode">graph,pivot</field>
<field name="view_mode">pivot</field>
<field name="search_view_id" ref="view_stock_level_forecast_filter"/>
<field name="context">{'search_default_product_id': active_id, 'group_by': ['company_id']}</field>
<field name="context">{'search_default_product_id': active_id, 'search_default_inventory_by_week':1}</field>
<field name="view_id" ref="view_stock_level_forecast_pivot"/>
</record>
<record id="action_stock_level_forecast_report_all_product" model="ir.actions.act_window">
<field name="name">Forecasted Inventory</field>
<field name="res_model">report.stock.forecast</field>
<field name="view_type">form</field>
<field name="view_mode">pivot</field>
<field name="search_view_id" ref="view_stock_level_forecast_filter"/>
<field name="domain">[('product_id.stock_move_ids.state', 'not in', ('draft', 'done', 'cancel'))]</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
There is nothing to forecast yet.
</p>
</field>
<field name="context">{'search_default_inventory_by_week':1, 'create': False}</field>
<field name="view_id" ref="view_stock_level_forecast_pivot"/>
</record>
</odoo>
+3
View File
@@ -210,6 +210,7 @@
<button type="action"
name="%(stock.action_stock_level_forecast_report_product)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
context="{'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">
@@ -292,6 +293,7 @@
<button type="action"
name="%(stock.action_stock_level_forecast_report_template)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
context="{'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">
@@ -383,6 +385,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.action_stock_level_forecast_report_all_product"/>
</data>
</odoo>