[IMP] stock{_account}: add (product) stock report

Adds a new reporting view for [storable] product.product to allow for
easier analysis by product (rather than by quant which is location
specific).

This includes:
- adding 3 new fields for products:
  - avg_cost = total svl value / total qty
  - total_value = avg_cost / on hand
  - company_currency_id = self.env.company.currency_id (i.e. the
    currently selected company) for which currency symbol to display in
    the view
  - IMPORTANT NOTE:
    - these variables (and some of the existing ones) do NOT support
      multi-company aggregations due these values being used to calcuate
      future svls which should be company specific. Larger refactoring
      would be needed to ensure correct calc in both cases. Larger issue
      occurs in stock.report which uses these variables in combination
      with variables that do take into account multi-company records.
- A customized `SearchPanel` so that a warehouse context can be applied
  to the view, rather than a filter. (viewable only when more than 1
  warehouse)

"stock report" part of b2b task: 2882539

Part-of: odoo/odoo#97109
This commit is contained in:
Tiffany Chang (tic)
2022-08-29 23:46:36 +02:00
parent 36a2514430
commit f05e3eabbe
12 changed files with 236 additions and 38 deletions
+6 -29
View File
@@ -69,15 +69,15 @@
'views/res_partner_views.xml',
'views/product_strategy_views.xml',
'views/product_views.xml',
'views/stock_lot_views.xml',
'views/stock_scrap_views.xml',
'views/stock_quant_views.xml',
'views/stock_location_views.xml',
'views/stock_warehouse_views.xml',
'views/stock_move_line_views.xml',
'views/stock_picking_views.xml',
'views/stock_move_views.xml',
'views/product_views.xml',
'views/stock_location_views.xml',
'views/stock_orderpoint_views.xml',
'views/stock_storage_category_views.xml',
'views/res_config_settings_views.xml',
@@ -103,24 +103,8 @@
'stock/static/src/scss/stock_traceability_report.scss',
],
'web.assets_backend': [
'stock/static/src/js/inventory_report_list_controller.js',
'stock/static/src/js/inventory_report_list_view.js',
'stock/static/src/js/inventory_singleton_list_renderer.js',
'stock/static/src/js/inventory_singleton_list_controller.js',
'stock/static/src/js/inventory_singleton_list_view.js',
'stock/static/src/js/report_stock_forecasted.js',
'stock/static/src/js/report_stock_reception.js',
'stock/static/src/js/lazy_column_list.js',
'stock/static/src/js/stock_orderpoint_list_controller.js',
'stock/static/src/js/stock_orderpoint_list_model.js',
'stock/static/src/js/stock_orderpoint_list_view.js',
'stock/static/src/js/stock_traceability_report_backend.js',
'stock/static/src/js/stock_traceability_report_widgets.js',
'stock/static/src/js/popover_widget.js',
'stock/static/src/js/forecast_widget.js',
'stock/static/src/js/counted_quantity_widget.js',
'stock/static/src/js/basic_model.js',
'stock/static/src/js/stock_rescheduling_popover.js',
'stock/static/src/**/*.js',
'stock/static/src/views/**/*.js',
'stock/static/src/scss/forecast_widget.scss',
'stock/static/src/scss/stock_empty_screen.scss',
],
@@ -134,15 +118,8 @@
'stock/static/tests/stock_traceability_report_backend_tests.js',
],
'web.assets_qweb': [
'stock/static/src/xml/inventory_report.xml',
'stock/static/src/xml/inventory_adjustments.xml',
'stock/static/src/xml/inventory_lines.xml',
'stock/static/src/xml/popover_widget.xml',
'stock/static/src/xml/forecast_widget.xml',
'stock/static/src/xml/report_stock_forecasted.xml',
'stock/static/src/xml/report_stock_reception.xml',
'stock/static/src/xml/stock_orderpoint.xml',
'stock/static/src/xml/stock_traceability_report_backend.xml',
'stock/static/src/**/*.xml',
'stock/static/src/views/**/*.xml',
],
},
'license': 'LGPL-3',
+4 -3
View File
@@ -516,7 +516,6 @@ class Product(models.Model):
# Be aware that the exact same function exists in product.template
def action_open_quants(self):
domain = [('product_id', 'in', self.ids)]
hide_location = not self.user_has_groups('stock.group_stock_multi_locations')
hide_lot = all(product.tracking == 'none' for product in self)
self = self.with_context(
@@ -544,8 +543,10 @@ class Product(models.Model):
else:
self = self.with_context(product_tmpl_ids=self.product_tmpl_id.ids)
action = self.env['stock.quant'].action_view_inventory()
action['domain'] = domain
action["name"] = _('Update Quantity')
# note that this action is used by different views w/varying customizations
if not self.env.context.get('is_stock_report'):
action['domain'] = [('product_id', 'in', self.ids)]
action["name"] = _('Update Quantity')
return action
def action_update_quantity_on_hand(self):
+3
View File
@@ -1074,3 +1074,6 @@ class Warehouse(models.Model):
'limit': 20,
'context': dict(self._context, default_warehouse_selectable=True, default_warehouse_ids=self.ids)
}
def get_current_warehouses(self):
return self.env['stock.warehouse'].search_read(fields=['id', 'name', 'code'], order='name')
@@ -0,0 +1,15 @@
/** @odoo-module **/
import { listView } from '@web/views/list/list_view';
import { registry } from "@web/core/registry";
import { StockReportSearchModel } from "../search/stock_report_search_model";
import { StockReportSearchPanel } from '../search/stock_report_search_panel';
export const StockReportListView = {
...listView,
SearchModel: StockReportSearchModel,
SearchPanel: StockReportSearchPanel,
};
registry.category("views").add("stock_report_list_view", StockReportListView);
@@ -0,0 +1,48 @@
/** @odoo-module **/
import { SearchModel } from "@web/search/search_model";
export class StockReportSearchModel extends SearchModel {
async load() {
await super.load(...arguments);
await this._loadWarehouses();
}
//---------------------------------------------------------------------
// Actions / Getters
//---------------------------------------------------------------------
getWarehouses() {
return this.warehouses;
}
async _loadWarehouses() {
this.warehouses = await this.orm.call(
'stock.warehouse',
'get_current_warehouses',
[[]],
{ context: this.context },
);
}
/**
* Clears the context of a warehouse so values calculate based on all possible
* warehouses
*/
clearWarehouseContext() {
delete this.globalContext.warehouse;
this._notify();
}
/**
* @param {number} warehouse_id
* Sets the context to the selected warehouse so values that take this into account
* will recalculate based on this without filtering out any records
*/
applyWarehouseContext(warehouse_id) {
this.globalContext['warehouse'] = warehouse_id;
this._notify();
}
}
@@ -0,0 +1,30 @@
/** @odoo-module **/
import { SearchPanel } from "@web/search/search_panel/search_panel";
export class StockReportSearchPanel extends SearchPanel {
setup() {
super.setup(...arguments);
this.selectedWarehouse = false;
}
//---------------------------------------------------------------------
// Actions / Getters
//---------------------------------------------------------------------
get warehouses() {
return this.env.searchModel.getWarehouses();
}
clearWarehouseContext() {
this.env.searchModel.clearWarehouseContext();
this.selectedWarehouse = null;
}
applyWarehouseContext(warehouse_id) {
this.env.searchModel.applyWarehouseContext(warehouse_id);
this.selectedWarehouse = warehouse_id;
}
}
StockReportSearchPanel.template = "stock.StockReportSearchPanel";
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.StockReportSearchPanel" t-inherit="web.SearchPanel" owl="1">
<xpath expr="//div/section[1]" position="before">
<section t-if="warehouses.length > 1" class="o_search_panel_section o_search_panel_warehouse">
<header class="o_search_panel_section_header pt-4 pb-2 text-uppercase o_cursor_default">
<i t-attf-class="fa fa-filter o_search_panel_section_icon text-warning me-2"/>
<b>Warehouses</b>
</header>
<ul class="list-group d-block o_search_panel_field">
<li class="o_search_panel_filter_value list-group-item p-0 mb-1 border-0 o_cursor_pointer">
<span t-on-click="ev => this.clearWarehouseContext(ev)" t-att-class="{'fw-bold': !selectedWarehouse}">All Warehouses</span>
</li>
<li class="o_search_panel_filter_value list-group-item p-0 mb-1 border-0 o_cursor_pointer"
t-foreach="warehouses" t-as="warehouse" t-key="warehouse.id">
<span t-out="warehouse.name" t-on-click="ev => this.applyWarehouseContext(warehouse.id, ev)"
t-att-class="{'fw-bold': selectedWarehouse === warehouse.id}"/>
</li>
</ul>
</section>
</xpath>
</t>
</templates>
+70
View File
@@ -450,6 +450,74 @@
</field>
</record>
<!-- Stock Report View -->
<record model="ir.ui.view" id="product_product_stock_tree">
<field name="name">product.product.stock.tree</field>
<field name="model">product.product</field>
<field name="priority" eval="100"/>
<field name="arch" type="xml">
<tree sample="1" js_class="stock_report_list_view">
<field name="id" invisible="1"/>
<field name="display_name" string="Product"/>
<field name="categ_id" optional="hide"/>
<field name="qty_available" string="On Hand" sum="Total On Hand"/>
<button name="%(action_view_inventory_tree)d" title="Inventory Adjustment" type="action" class="btn-link"
icon="fa-pencil" context="{'search_default_product_id': id, 'default_product_id': id}"/>
<field name="free_qty" string="Free to Use" sum="Total Free to Use"/>
<field name="incoming_qty" optional="show" sum="Total Incoming"/>
<field name="outgoing_qty" optional="show" sum="Total Outgoing"/>
<field name="virtual_available" string="Forecasted" optional="hide" sum="Total Forecasted"/>
<field name="uom_id" string="Unit" groups="uom.group_uom" options="{'no_create': True}"/>
<button name="%(stock_move_line_action)d" string="History" type="action" class="btn-link"
icon="fa-history" context="{'search_default_product_id': id, 'default_product_id': id}"/>
<button name="action_view_orderpoints" string="Replenishment" type="object" class="btn-link"
icon="fa-refresh" context="{'search_default_product_id': id, 'is_stock_report': True}" />
<button name="%(action_view_quants)d" string="Locations" type="action" class="btn-link" groups="stock.group_stock_multi_locations"
icon="fa-cubes" context="{'search_default_product_id': id, 'default_product_id': id}" attrs="{'invisible': [('qty_available', '=', 0)]}"/>
<button name="action_product_forecast_report" string="Forecast" type="object" class="btn-link"
icon="fa-area-chart" context="{'default_product_id': id}" attrs="{'invisible': [('incoming_qty', '=', 0), ('outgoing_qty', '=', 0)]}"/>
</tree>
</field>
</record>
<record id="product_search_form_view_stock_report" model="ir.ui.view">
<field name="name">product.product.search.stock.form.stock.report</field>
<field name="model">product.product</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="stock_product_search_form_view"/>
<field name="arch" type="xml">
<filter name='services' position="attributes">
<attribute name="attrs">{'invisible': 1}</attribute>
</filter>
<filter name='consumable' position="attributes">
<attribute name="attrs">{'invisible': 1}</attribute>
</filter>
<filter name="real_stock_negative" position="after">
<searchpanel>
<field name="categ_id" icon="fa-filter" string="Category" select="multi"/>
</searchpanel>
</filter>
</field>
</record>
<record model="ir.actions.act_window" id="action_product_stock_view">
<field name="name">Stock</field>
<field name="res_model">product.product</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="product_product_stock_tree"/>
<field name="search_view_id" ref="product_search_form_view_stock_report"/>
<field name="context">{'default_detailed_type': 'product'}</field>
<field name="domain">[('detailed_type', '=', 'product')]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No stock to show
</p><p>
Create some storable products to see their stock info in this view.
</p>
</field>
</record>
<record id="product_template_action_product" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="type">ir.actions.act_window</field>
@@ -479,6 +547,8 @@
<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_product_stock" name="Stock" action="stock.action_product_stock_view"
parent="stock.menu_warehouse_report" sequence="5"/>
</data>
</odoo>
@@ -43,7 +43,7 @@
<field name="company_id" invisible="1"/>
<field name="product_category_id" invisible="1"/>
<field name="product_tmpl_id" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('product_id', '!=', False)]}" invisible="context.get('default_product_id')" force_save="1"/>
<field name="product_id" attrs="{'readonly': [('product_id', '!=', False)]}" force_save="1"/>
<field name="location_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="warehouse_id" options="{'no_create': True}" groups="stock.group_stock_multi_warehouses" optional="hide"/>
<field name="qty_on_hand" force_save="1"/>
+1 -1
View File
@@ -29,10 +29,10 @@ Dashboard / Reports for Warehouse Management includes:
'views/stock_account_views.xml',
'views/res_config_settings_views.xml',
'data/product_data.xml',
'views/product_views.xml',
'views/report_invoice.xml',
'views/stock_valuation_layer_views.xml',
'views/stock_quant_views.xml',
'views/product_views.xml',
'wizard/stock_request_count.xml',
'wizard/stock_valuation_layer_revaluation_views.xml',
'wizard/stock_quantity_history.xml',
+17 -4
View File
@@ -96,6 +96,12 @@ class ProductProduct(models.Model):
value_svl = fields.Float(compute='_compute_value_svl', compute_sudo=True)
quantity_svl = fields.Float(compute='_compute_value_svl', compute_sudo=True)
avg_cost = fields.Monetary(string="Average Cost", compute='_compute_value_svl', compute_sudo=True, currency_field='company_currency_id')
total_value = fields.Monetary(string="Total Value", compute='_compute_value_svl', compute_sudo=True, currency_field='company_currency_id')
company_currency_id = fields.Many2one(
'res.currency', 'Valuation Currency', compute='_compute_value_svl', compute_sudo=True,
help="Technical field to correctly show the currently selected company's currency that corresponds "
"to the totaled value of the product's valuation layers")
stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'product_id')
valuation = fields.Selection(related="categ_id.property_valuation", readonly=True)
cost_method = fields.Selection(related="categ_id.property_cost_method", readonly=True)
@@ -108,11 +114,12 @@ class ProductProduct(models.Model):
@api.depends('stock_valuation_layer_ids')
@api.depends_context('to_date', 'company')
def _compute_value_svl(self):
"""Compute `value_svl` and `quantity_svl`."""
company_id = self.env.company.id
"""Compute totals of multiple svl related values"""
company_id = self.env.company
self.company_currency_id = company_id.currency_id
domain = [
('product_id', 'in', self.ids),
('company_id', '=', company_id),
('company_id', '=', company_id.id),
]
if self.env.context.get('to_date'):
to_date = fields.Datetime.to_datetime(self.env.context['to_date'])
@@ -121,12 +128,18 @@ class ProductProduct(models.Model):
products = self.browse()
for group in groups:
product = self.browse(group['product_id'][0])
product.value_svl = self.env.company.currency_id.round(group['value'])
value_svl = company_id.currency_id.round(group['value'])
avg_cost = value_svl / group['quantity'] if group['quantity'] else 0
product.value_svl = value_svl
product.quantity_svl = group['quantity']
product.avg_cost = avg_cost
product.total_value = avg_cost * product.sudo(False).qty_available
products |= product
remaining = (self - products)
remaining.value_svl = 0
remaining.quantity_svl = 0
remaining.avg_cost = 0
remaining.total_value = 0
# -------------------------------------------------------------------------
# Actions
@@ -45,5 +45,22 @@
</group>
</field>
</record>
<!-- Stock Report View -->
<record model="ir.ui.view" id="product_product_stock_tree_inherit_stock_account">
<field name="name">product.product.stock.tree.inherit.stock.account</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="stock.product_product_stock_tree"/>
<field name="arch" type="xml">
<field name="qty_available" position="before">
<field name="company_currency_id" invisible="1"/>
<field name="cost_method" invisible="1"/>
<field name="avg_cost" string="Unit Cost" optional="show" widget='monetary' options="{'currency_field': 'company_currency_id'}"/>
<field name="total_value" string="Total Value" optional="show" widget='monetary' options="{'currency_field': 'company_currency_id'}" sum="Total Value"/>
<button name="%(stock_valuation_layer_action)d" title="Valuation Report" type="action" class="btn-link"
icon="fa-bar-chart" context="{'search_default_product_id': id, 'default_product_id': id}" attrs="{'invisible': [('cost_method', '!=', 'fifo')]}"/>
</field>
</field>
</record>
</data>
</odoo>