[IMP] stock{_account}: inventory => locations report update

Inventory report has been updated to have:
- better applicability: report renamed to "Locations" and is only
  visible w/Locations, Consignment, or debug mode active
- improved UX: list view rearranged, buttons added, grouping removed,
  sums added to bottom of list
- improved quant "Value": instead of the accounting value, this is now
  the average unit cost (i.e. sum(valuation layer values)/sum(valuation
  layer quantities) per product x on hand qty (of quant).
- single click load for products
- search based on Warehouse option (including storing
  location.warehouse_id to avoid overly complex search function)
- always show "Location" column in this view even if multi-locations is
  not active

"inventory report" part of b2b task: 2882539

Upgrade PR: odoo/upgrade#3819

Part-of: odoo/odoo#97109
This commit is contained in:
Tiffany Chang (tic)
2022-08-29 23:46:34 +02:00
parent a60071ae2d
commit 9978bcb366
11 changed files with 67 additions and 60 deletions
@@ -21,9 +21,9 @@
<field name="model">stock.quant</field>
<field name="inherit_id" ref="stock.view_stock_quant_tree_editable"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='inventory_quantity_auto_apply']" position="before">
<xpath expr="//field[@name='lot_id']" position="after">
<field name="use_expiration_date" invisible="1"/>
<field name="removal_date" optional="show"
<field name="removal_date" optional="hide"
invisible="context.get('hide_removal_date')" attrs="{'readonly': ['|', ('tracking', '=', 'none'), ('use_expiration_date', '=', False)]}"/>
</xpath>
</field>
+1 -1
View File
@@ -90,7 +90,7 @@ class Location(models.Model):
last_inventory_date = fields.Date("Last Effective Inventory", readonly=True, help="Date of the last inventory at this location.")
next_inventory_date = fields.Date("Next Expected Inventory", compute="_compute_next_inventory_date", store=True, help="Date for next planned inventory based on cyclic schedule.")
warehouse_view_ids = fields.One2many('stock.warehouse', 'view_location_id', readonly=True)
warehouse_id = fields.Many2one('stock.warehouse', compute='_compute_warehouse_id')
warehouse_id = fields.Many2one('stock.warehouse', compute='_compute_warehouse_id', store=True)
storage_category_id = fields.Many2one('stock.storage.category', string='Storage Category', check_company=True)
outgoing_move_line_ids = fields.One2many('stock.move.line', 'location_id') # used to compute weight
incoming_move_line_ids = fields.One2many('stock.move.line', 'location_dest_id') # used to compute weight
+9 -2
View File
@@ -3,6 +3,7 @@
import logging
from ast import literal_eval
from psycopg2 import Error
from odoo import _, api, fields, models
@@ -67,6 +68,7 @@ class StockQuant(models.Model):
'stock.location', 'Location',
domain=lambda self: self._domain_location_id(),
auto_join=True, ondelete='restrict', required=True, index=True, check_company=True)
warehouse_id = fields.Many2one('stock.warehouse', related='location_id.warehouse_id')
storage_category_id = fields.Many2one(related='location_id.storage_category_id', store=True)
cyclic_inventory_frequency = fields.Integer(related='location_id.cyclic_inventory_frequency')
lot_id = fields.Many2one(
@@ -340,6 +342,11 @@ class StockQuant(models.Model):
]
return action
def action_view_orderpoints(self):
action = self.env['product.product'].action_view_orderpoints()
action['domain'] = [('product_id', '=', self.product_id.id)]
return action
@api.model
def action_view_quants(self):
self = self.with_context(search_default_internal_loc=1)
@@ -923,7 +930,7 @@ class StockQuant(models.Model):
company_user = self.env.company
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
if warehouse:
self = self.with_context(default_location_id=warehouse.lot_stock_id.id, hide_location=True)
self = self.with_context(default_location_id=warehouse.lot_stock_id.id, hide_location=not self.env.context.get('always_show_loc', False))
# If user have rights to write on quant, we set quants in inventory mode.
if self.user_has_groups('stock.group_stock_user'):
@@ -945,7 +952,7 @@ class StockQuant(models.Model):
ctx['inventory_report_mode'] = True
ctx.pop('group_by', None)
action = {
'name': _('Stock On Hand'),
'name': _('Locations'),
'view_type': 'tree',
'view_mode': 'list,form',
'res_model': 'stock.quant',
+4
View File
@@ -144,6 +144,10 @@ class Warehouse(models.Model):
if vals.get('partner_id'):
self._update_partner_data(vals['partner_id'], vals.get('company_id'))
# manually update locations' warehouse since it didn't exist at their creation time
view_location_id = self.env['stock.location'].browse(vals.get('view_location_id'))
(view_location_id | view_location_id.with_context(active_test=False).child_ids).write({'warehouse_id': warehouse.id})
self._check_multiwarehouse_group()
return warehouses
@@ -44,6 +44,9 @@ var SingletonListController = InventoryReportListController.extend({
*/
renderButtons: function ($node) {
this._super(...arguments);
if (this.context.inventory_report_mode) {
this.$buttons.find('button.o_button_apply_all').hide();
}
this.$buttons.on('click', '.o_button_apply_all', this._onApplyAll.bind(this));
},
+1 -1
View File
@@ -177,7 +177,7 @@
<field name="type">ir.actions.act_window</field>
<field name="view_mode">tree,kanban,pivot,form</field>
<field name="view_id" ref="view_move_line_tree"/>
<field name="context">{'search_default_filter_last_12_months': 1, 'search_default_done': 1, 'search_default_groupby_product_id': 1, 'create': 0}</field>
<field name="context">{'search_default_filter_last_12_months': 1, 'search_default_done': 1, 'create': 0}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
There's no product move yet
@@ -51,7 +51,7 @@
<button name="action_product_forecast_report" type="object" icon="fa-area-chart" title="Forecast Report" attrs="{'invisible': [('id', '=', False)]}"/>
<field name="visibility_days" optional="hidden"/>
<field name="route_id" options="{'no_create': True, 'no_open': True}"/>
<button name="action_stock_replenishment_info" type="object" icon="fa-info-circle" title="Resplenishment Information" attrs="{'invisible': [('id', '=', False)]}"/>
<button name="action_stock_replenishment_info" type="object" icon="fa-info-circle" title="Replenishment Information" attrs="{'invisible': [('id', '=', False)]}"/>
<field name="trigger" optional="hide"/>
<field name="group_id" optional="hide" groups="stock.group_adv_location"/>
<field name="product_min_qty" optional="show"/>
+33 -29
View File
@@ -1,6 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.actions.server" id="action_view_inventory_tree"> <!-- Menu + python -->
<field name="name">Inventory</field>
<field name="model_id" ref="model_stock_quant"/>
<field name="state">code</field>
<field name="groups_id" eval="[(4, ref('stock.group_stock_user'))]"/>
<field name="code">
action = model.action_view_inventory()
</field>
</record>
<record id="quant_search_view" model="ir.ui.view">
<field name="name">stock.quant.search</field>
<field name="model">stock.quant</field>
@@ -9,6 +19,7 @@
<search string="Quants">
<field name="product_id"/>
<field name="location_id"/>
<field name="warehouse_id"/>
<field name="storage_category_id" groups="stock.group_stock_storage_categories" />
<field name="user_id"/>
<field name="inventory_date"/>
@@ -83,11 +94,11 @@
</field>
</record>
<!-- Inventory Report + Update Quantity (via product) view -->
<!-- Location Report view -->
<record model="ir.ui.view" id="view_stock_quant_tree_editable">
<field name="name">stock.quant.tree.editable</field>
<field name="model">stock.quant</field>
<field eval="10" name="priority"/>
<field eval="5" name="priority"/>
<field name="arch" type="xml">
<tree editable="bottom"
create="1" edit="1" js_class="singleton_list"
@@ -95,13 +106,17 @@
<field name="id" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('id', '!=', False)]}"
readonly="context.get('single_product', False)" force_save="1"
options="{'no_create': True}"/>
<field name="product_categ_id" optional="hide"/>
<field name="location_id" attrs="{'readonly': [('id', '!=', False)]}"
invisible="context.get('hide_location', False)"
options="{'no_create': True}"/>
<field name="storage_category_id" optional="hide"/>
<field name="product_id" attrs="{'readonly': [('id', '!=', False)]}"
readonly="context.get('single_product', False)" force_save="1"
options="{'no_create': True}" widget="many2one"/>
<field name="product_categ_id" optional="hide"/>
<field name='company_id' groups="base.group_multi_company" optional="hidden"/>
<field name="package_id" groups="stock.group_tracking_lot"
attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="lot_id" groups="stock.group_production_lot"
attrs="{
'readonly': ['|', ('id', '!=', False), ('tracking', 'not in', ['serial', 'lot'])],
@@ -109,15 +124,15 @@
}"
invisible="context.get('hide_lot', False)"
context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
<field name="package_id" groups="stock.group_tracking_lot"
attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="owner_id" groups="stock.group_tracking_owner"
attrs="{'readonly': [('id', '!=', False)]}"
options="{'no_create': True}"/>
<field name="inventory_quantity_auto_apply" string="On Hand Quantity" readonly="0"/>
<field name="available_quantity" optional="show"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name='company_id' groups="base.group_multi_company" optional="show"/>
<field name="inventory_quantity_auto_apply" string="On Hand Quantity" readonly="0" 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': product_id, 'default_product_id': product_id}"/>
<field name="reserved_quantity" optional="show" sum="Total Reserved"/>
<field name="product_uom_id" string="Unit" groups="uom.group_uom"/>
<button name="action_view_stock_moves" string="History" type="object" class="btn-link" icon="fa-history"/>
<button name="action_view_orderpoints" string="Replenishment" type="object" context="{'default_product_id': product_id, 'search_default_location_id': location_id}" class="btn-link" icon="fa-refresh"/>
</tree>
</field>
</record>
@@ -174,16 +189,15 @@
<field name="code">
action = model.with_context(
search_default_internal_loc=1,
search_default_productgroup=1,
search_default_locationgroup=1,
always_show_loc=1,
).action_view_quants()
</field>
</record>
<record model="ir.actions.act_window" id="dashboard_open_quants"> <!-- Used in dashboard -->
<field name="name">Stock On Hand</field>
<field name="context">{'search_default_internal_loc': 1, 'search_default_productgroup':1, 'search_default_locationgroup':1}</field>
<field name="name">Locations</field>
<field name="res_model">stock.quant</field>
<field name="context">{}</field>
</record>
<record model="ir.actions.act_window" id="location_open_quants"> <!-- Used in location -->
@@ -322,7 +336,7 @@
</field>
</record>
<!-- Inventory Adjustments view -->
<!-- Inventory Adjustments + Update Quantity (via product) view -->
<record model="ir.ui.view" id="view_stock_quant_tree_inventory_editable">
<field name="name">stock.quant.inventory.tree.editable</field>
<field name="model">stock.quant</field>
@@ -384,19 +398,9 @@
</field>
</record>
<record model="ir.actions.server" id="action_view_inventory_tree"> <!-- Menu + python -->
<field name="name">Inventory</field>
<field name="model_id" ref="model_stock_quant"/>
<field name="state">code</field>
<field name="groups_id" eval="[(4, ref('stock.group_stock_user'))]"/>
<field name="code">
action = model.action_view_inventory()
</field>
</record>
<menuitem id="menu_action_inventory_tree" name="Inventory Adjustments" parent="menu_stock_warehouse_mgmt" sequence="10" action="action_view_inventory_tree" />
<menuitem id="menu_package" name="Packages" parent="menu_stock_inventory_control" action="action_package_view" groups="stock.group_tracking_lot" sequence="102"/>
<menuitem id="menu_valuation" name="Inventory Report"
<menuitem id="menu_valuation" name="Locations"
parent="stock.menu_warehouse_report" sequence="150"
action="action_view_quants" groups="stock.group_stock_multi_locations,stock.group_tracking_owner"/>
action="action_view_quants" groups="stock.group_stock_multi_locations,stock.group_tracking_owner,base.group_no_one"/>
</odoo>
+1 -1
View File
@@ -30,9 +30,9 @@ Dashboard / Reports for Warehouse Management includes:
'views/res_config_settings_views.xml',
'data/product_data.xml',
'views/product_views.xml',
'views/stock_quant_views.xml',
'views/report_invoice.xml',
'views/stock_valuation_layer_views.xml',
'views/stock_quant_views.xml',
'wizard/stock_request_count.xml',
'wizard/stock_valuation_layer_revaluation_views.xml',
'report/report_stock_forecasted.xml',
+10 -22
View File
@@ -19,33 +19,21 @@ class StockQuant(models.Model):
@api.depends('company_id', 'location_id', 'owner_id', 'product_id', 'quantity')
def _compute_value(self):
""" For standard and AVCO valuation, compute the current accounting
valuation of the quants by multiplying the quantity by
the standard price. Instead for FIFO, use the quantity times the
average cost (valuation layers are not manage by location so the
average cost is the same for all location and the valuation field is
a estimation more than a real value).
""" (Product.value_svl / Product.quantity_svl) * quant.quantity, i.e. average unit cost * on hand qty
"""
for quant in self:
quant.currency_id = quant.company_id.currency_id
# If the user didn't enter a location yet while enconding a quant.
if not quant.location_id:
quant.value = 0
return
if not quant.location_id._should_be_valued() or\
(quant.owner_id and quant.owner_id != quant.company_id.partner_id):
if not quant.location_id or not quant.product_id or\
not quant.location_id._should_be_valued() or\
(quant.owner_id and quant.owner_id != quant.company_id.partner_id) or\
float_is_zero(quant.quantity, precision_rounding=quant.product_id.uom_id.rounding):
quant.value = 0
continue
if quant.product_id.cost_method == 'fifo':
quantity = quant.product_id.with_company(quant.company_id).quantity_svl
if float_is_zero(quantity, precision_rounding=quant.product_id.uom_id.rounding):
quant.value = 0.0
continue
average_cost = quant.product_id.with_company(quant.company_id).value_svl / quantity
quant.value = quant.quantity * average_cost
else:
quant.value = quant.quantity * quant.product_id.with_company(quant.company_id).standard_price
quantity = quant.product_id.with_company(quant.company_id).quantity_svl
if float_is_zero(quantity, precision_rounding=quant.product_id.uom_id.rounding):
quant.value = 0.0
continue
quant.value = quant.quantity * quant.product_id.with_company(quant.company_id).value_svl / quantity
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
@@ -19,7 +19,8 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='product_uom_id']" position="after">
<field name="currency_id" invisible="1"/>
<field name="value" optional="hidden"/>
<field name="value" optional="hidden" sum="Total Value"/>
<button name="%(stock_valuation_layer_action)d" title="Stock Valuation" type="action" class="btn-link" icon="fa-bar-chart" context="{'search_default_product_id': product_id}"/>
</xpath>
</field>
</record>