[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:
committed by
Arnold Moyaux
parent
7e49fda883
commit
d3f14f6a1d
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user