[IMP] stock: Inventory Adjustments improvements

Improvements of the Inventory Adjustements page, among which:
 - Add a new 'Apply All' button (without the need to select a record)
 - Show the date at which the last count was done
 - Add warning icon next to duplicated SN
 - Allows to change the lot_id when empty and no quantities are set

closes odoo/odoo#78361

Signed-off-by: Arnold Moyaux <arm@odoo.com>
This commit is contained in:
Nicolas Pierre
2021-12-17 10:10:45 +00:00
parent aa1134ac05
commit bec3c58df3
8 changed files with 158 additions and 10 deletions
+1
View File
@@ -135,6 +135,7 @@
],
'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',
+75 -2
View File
@@ -61,15 +61,19 @@ class StockQuant(models.Model):
product_uom_id = fields.Many2one(
'uom.uom', 'Unit of Measure',
readonly=True, related='product_id.uom_id')
priority = fields.Selection(related='product_tmpl_id.priority')
company_id = fields.Many2one(related='location_id.company_id', string='Company', store=True, readonly=True)
location_id = fields.Many2one(
'stock.location', 'Location',
domain=lambda self: self._domain_location_id(),
auto_join=True, ondelete='restrict', required=True, index=True, check_company=True)
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(
'stock.lot', 'Lot/Serial Number', index=True,
ondelete='restrict', check_company=True,
domain=lambda self: self._domain_lot_id())
sn_duplicated = fields.Boolean(string="Duplicated Serial Number", compute='_compute_sn_duplicated', help="If the same SN is in another Quant")
package_id = fields.Many2one(
'stock.quant.package', 'Package',
domain="[('location_id', '=', location_id)]",
@@ -110,6 +114,7 @@ class StockQuant(models.Model):
inventory_date = fields.Date(
'Scheduled Date', compute='_compute_inventory_date', store=True, readonly=False,
help="Next date the On Hand Quantity should be counted.")
last_count_date = fields.Date(compute='_compute_last_count_date', help='Last time the Quantity was Updated')
inventory_quantity_set = fields.Boolean(store=True, compute='_compute_inventory_quantity_set', readonly=False)
is_outdated = fields.Boolean('Quantity has been moved since last count', compute='_compute_is_outdated')
user_id = fields.Many2one(
@@ -127,6 +132,56 @@ class StockQuant(models.Model):
for quant in quants:
quant.inventory_date = date_by_location[quant.location_id]
def _compute_last_count_date(self):
""" We look at the stock move lines associated with every quant to get the last count date.
"""
self.last_count_date = False
groups = self.env['stock.move.line'].read_group(
[
('state', '=', 'done'),
('is_inventory', '=', True),
('product_id', 'in', self.product_id.ids),
'|',
('lot_id', 'in', self.lot_id.ids),
('lot_id', '=', False),
'|',
('owner_id', 'in', self.owner_id.ids),
('owner_id', '=', False),
'|',
('location_id', 'in', self.location_id.ids),
('location_dest_id', 'in', self.location_id.ids),
'|',
('package_id', '=', False),
'|',
('package_id', 'in', self.package_id.ids),
('result_package_id', 'in', self.package_id.ids),
],
['date:max', 'product_id', 'lot_id', 'package_id', 'owner_id', 'result_package_id', 'location_id', 'location_dest_id'],
['product_id', 'lot_id', 'package_id', 'owner_id', 'result_package_id', 'location_id', 'location_dest_id'],
lazy=False)
def _update_dict(date_by_quant, key, value):
current_date = date_by_quant.get(key)
if not current_date or value > current_date:
date_by_quant[key] = value
date_by_quant = {}
for group in groups:
move_line_date = group['date']
location_id = group['location_id'][0]
location_dest_id = group['location_dest_id'][0]
package_id = group['package_id'] and group['package_id'][0]
result_package_id = group['result_package_id'] and group['result_package_id'][0]
lot_id = group['lot_id'] and group['lot_id'][0]
owner_id = group['owner_id'] and group['owner_id'][0]
product_id = group['product_id'][0]
_update_dict(date_by_quant, (location_id, package_id, product_id, lot_id, owner_id), move_line_date)
_update_dict(date_by_quant, (location_dest_id, package_id, product_id, lot_id, owner_id), move_line_date)
_update_dict(date_by_quant, (location_id, result_package_id, product_id, lot_id, owner_id), move_line_date)
_update_dict(date_by_quant, (location_dest_id, result_package_id, product_id, lot_id, owner_id), move_line_date)
for quant in self:
quant.last_count_date = date_by_quant.get((quant.location_id.id, quant.package_id.id, quant.product_id.id, quant.lot_id.id, quant.owner_id.id))
@api.depends('inventory_quantity')
def _compute_inventory_diff_quantity(self):
for quant in self:
@@ -148,6 +203,15 @@ class StockQuant(models.Model):
for quant in self:
quant.inventory_quantity_auto_apply = quant.quantity
@api.depends('lot_id')
def _compute_sn_duplicated(self):
self.sn_duplicated = False
domain = [('tracking', '=', 'serial'), ('lot_id', 'in', self.lot_id.ids), ('location_id.usage', 'in', ['internal', 'transit'])]
results = self.read_group(domain, ['lot_id'], ['lot_id'])
duplicated_sn_ids = [x['lot_id'][0] for x in results if x['lot_id_count'] > 1]
quants_with_duplicated_sn = self.env['stock.quant'].search([('lot_id', 'in', duplicated_sn_ids)])
quants_with_duplicated_sn.sn_duplicated = True
def _set_inventory_quantity(self):
""" Inverse method to create stock move when `inventory_quantity` is set
(`inventory_quantity` is only accessible in inventory mode).
@@ -309,7 +373,7 @@ class StockQuant(models.Model):
and fields.Float.is_zero(quant.quantity, precision_rounding=rounding):
continue
if quant.product_id.tracking in ['lot', 'serial'] and\
not quant.lot_id and quant.inventory_quantity != quant.quantity:
not quant.lot_id and quant.inventory_quantity != quant.quantity and not quant.quantity:
products_tracked_without_lot.append(quant.product_id.id)
# for some reason if multi-record, env.context doesn't pass to wizards...
ctx = dict(self.env.context or {})
@@ -408,6 +472,15 @@ class StockQuant(models.Model):
self.inventory_diff_quantity = 0
self.inventory_quantity_set = False
def action_warning_duplicated_sn(self):
return {
'name': _('Warning Duplicated SN'),
'type': 'ir.actions.act_window',
'res_model': 'stock.quant',
'views': [(self.env.ref('stock.duplicated_sn_warning').id, 'form')],
'target': 'new',
}
@api.constrains('product_id')
def check_product_id(self):
if any(elem.product_id.type != 'product' for elem in self):
@@ -782,7 +855,7 @@ class StockQuant(models.Model):
""" Returns a list of fields user can edit when he want to edit a quant in `inventory_mode`.
"""
fields = ['inventory_quantity', 'inventory_quantity_auto_apply', 'inventory_diff_quantity',
'inventory_date', 'user_id', 'inventory_quantity_set', 'is_outdated']
'inventory_date', 'user_id', 'inventory_quantity_set', 'is_outdated', 'lot_id']
return fields
def _get_inventory_move_values(self, qty, location_id, location_dest_id, out=False):
@@ -22,6 +22,30 @@ var _t = core._t;
var SingletonListController = InventoryReportListController.extend({
init: function (parent, model, renderer, params) {
this.context = renderer.state.getContext();
return this._super(...arguments);
},
willStart() {
const sup = this._super(...arguments);
const acl = this.getSession().user_has_group('stock.group_stock_manager').then(hasGroup => {
if (hasGroup) {
this.buttons_template = 'StockInventoryAdjustments.Buttons';
}
});
return Promise.all([sup, acl]);
},
/**
* @override
*/
renderButtons: function ($node) {
this._super(...arguments);
this.$buttons.on('click', '.o_button_apply_all', this._onApplyAll.bind(this));
},
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
@@ -95,6 +119,16 @@ var SingletonListController = InventoryReportListController.extend({
self._enableButtons();
}).guardedCatch(this._enableButtons.bind(this));
},
_onApplyAll: function () {
this.do_action('stock.action_stock_inventory_adjustement_name', {
additional_context: {
'active_model': 'ir.ui.view',
'active_ids': this.renderer.state.data.map(el => el['res_id']),
},
on_close: this.reload,
});
},
});
return SingletonListController;
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-extend="ListView.buttons" t-name="StockInventoryAdjustments.Buttons">
<t t-jquery="button.o_list_button_add" t-operation="after">
<button class="btn btn-secondary o_button_apply_all" type="button">
Apply All
</button>
</t>
</t>
</templates>
+1 -1
View File
@@ -40,7 +40,7 @@
<field name="scrap_location" attrs="{'invisible': [('usage', 'not in', ('inventory', 'internal'))]}"/>
<field name="return_location"/>
</group>
<group string="Cyclic Inventory" attrs="{'invisible': ['|', ('usage', 'not in', ('internal', 'transit')), ('company_id', '=', False)]}">
<group string="Cyclic Counting" attrs="{'invisible': ['|', ('usage', 'not in', ('internal', 'transit')), ('company_id', '=', False)]}">
<field name="cyclic_inventory_frequency"/>
<field name="last_inventory_date"/>
<field name="next_inventory_date" attrs="{'invisible': [('active', '=', False)]}"/>
+28 -3
View File
@@ -9,6 +9,7 @@
<search string="Quants">
<field name="product_id"/>
<field name="location_id"/>
<field name="storage_category_id" groups="stock.group_stock_storage_categories" />
<field name="user_id"/>
<field name="inventory_date"/>
<field name="product_categ_id"/>
@@ -22,6 +23,7 @@
<filter name="on_hand" string="On Hand" domain="[('on_hand', '=', True)]"/>
<filter name="to_count" string="To Count" domain="[('inventory_date', '&lt;=', context_today().strftime('%Y-%m-%d'))]"/>
<filter name="to_apply" string="To Apply" domain="[('inventory_quantity_set', '=', True)]"/>
<filter name="priority_products" string="Starred Products" domain="[('priority', '=', 1)]"/>
<separator/>
<filter name="negative" string="Negative Stock" domain="[('quantity', '&lt;', 0.0)]"/>
<filter name="reserved" string="Reservations" domain="[('reserved_quantity', '&gt;', 0.0)]"/>
@@ -33,6 +35,7 @@
<group expand='0' string='Group by...'>
<filter string='Product' name="productgroup" context="{'group_by': 'product_id'}"/>
<filter string='Location' name="locationgroup" domain="[]" context="{'group_by': 'location_id'}"/>
<filter string='Storage Category' name="storagecategorygroup" domain="[]" context="{'group_by': 'storage_category_id'}"/>
<filter string='Owner' name="owner" context="{'group_by': 'owner_id'}" groups="stock.group_tracking_owner"/>
<filter string='Lot/Serial Number' name="Lot_Serial_number" context="{'group_by': 'lot_id'}" groups="stock.group_production_lot"/>
<filter string='Package' name="package" domain="[]" context="{'group_by': 'package_id'}" groups="stock.group_tracking_lot"/>
@@ -298,6 +301,20 @@
</field>
</record>
<record id="duplicated_sn_warning" model="ir.ui.view">
<field name="name">stock.quant.duplicated.sn.warning</field>
<field name="priority">1000</field>
<field name="model">stock.quant</field>
<field name="arch" type="xml">
<form string="Duplicated SN Warning">
This SN is already in another location.
<footer>
<button string="Close" class="btn btn-primary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Inventory Adjustments view -->
<record model="ir.ui.view" id="view_stock_quant_tree_inventory_editable">
<field name="name">stock.quant.inventory.tree.editable</field>
@@ -312,19 +329,27 @@
</header>
<field name="id" invisible="1"/>
<field name="is_outdated" invisible="1"/>
<field name="sn_duplicated" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="inventory_quantity_set" invisible="1"/>
<field name="location_id" domain="[('usage', 'in', ['internal', 'transit'])]" attrs="{'readonly': [('id', '!=', False)]}" invisible="context.get('hide_location', False)" options="{'no_create': True}"/>
<field name="storage_category_id" groups="stock.group_stock_storage_categories" invisible="context.get('hide_location', False)" options="{'no_create': True}" optional="hidden"/>
<field name="cyclic_inventory_frequency" invisible="context.get('hide_location', False)" options="{'no_create': True}" optional="hidden"/>
<field name="priority" widget="priority" nolabel="1" optional="hidden"/>
<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"/>
<button name="action_warning_duplicated_sn" type="object" attrs="{'invisible': [('sn_duplicated', '=', False)]}" title="This lot/serial number is already in another location" class="btn btn-secondary text-warning pull-right" icon="fa-warning"/>
<field name="lot_id" groups="stock.group_production_lot"
attrs="{'readonly': ['|', ('id', '!=', False), ('tracking', 'not in', ['serial', 'lot'])]}"
attrs="{'readonly': ['|', ('tracking', 'not in', ['serial', 'lot']), '&amp;', ('id', '!=', False), '|', ('lot_id', '!=', False), ('quantity', '!=', 0)]}"
invisible="context.get('hide_lot', False)"
context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
context="{'default_product_id': product_id, 'default_company_id': company_id}"
decoration-warning="sn_duplicated"
force_save="1"/>
<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="last_count_date" optional='hidden' readonly='1'/>
<field name="available_quantity" string="Available Quantity" decoration-danger="available_quantity &lt; 0" optional="hidden"/>
<field name="quantity" optional="show" string="On Hand Quantity"/>
<field name="quantity" optional="show" decoration-warning="quantity &lt; 0" string="On Hand Quantity"/>
<field name="product_uom_id" groups="uom.group_uom" string="UoM"/>
<field name="inventory_quantity" widget="counted_quantity_widget"/>
<field name="inventory_diff_quantity" string="Difference" attrs="{'invisible': [('inventory_quantity_set', '=', False)]}" decoration-muted="inventory_diff_quantity == 0" decoration-danger="inventory_diff_quantity &lt; 0" decoration-success="inventory_diff_quantity &gt; 0" decoration-bf="inventory_diff_quantity != 0"/>
@@ -23,5 +23,5 @@ class StockInventoryAdjustmentName(models.TransientModel):
show_info = fields.Boolean('Show warning')
def action_apply(self):
return self.quant_ids.with_context(
inventory_name=self.inventory_adjustment_name).action_apply_inventory()
quants = self.quant_ids.filtered('inventory_quantity_set')
quants.with_context(inventory_name=self.inventory_adjustment_name).action_apply_inventory()
@@ -7,7 +7,10 @@
<form string="Lots or serial numbers were not provided for tracked products">
<field name="product_ids" invisible="1"/>
<field name="quant_ids" invisible="1"/>
<p>Some products of the inventory adjustment are tracked. Are you sure you don't want to specify a serial or lot number for them?</p>
<p>
Those products you added are tracked but lots/serials were not defined. Once applied those can't be changed.<br/>
Apply anyway?
</p>
<strong>Tracked product(s):</strong>
<field name="tracking_line_ids" readonly="1">
<tree>
@@ -16,7 +19,7 @@
</tree>
</field>
<footer>
<button name="action_confirm" string="Confirm" type="object" class="btn-primary" data-hotkey="q"/>
<button name="action_confirm" string="Apply" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Discard" special="cancel" data-hotkey="z" class="btn-secondary"/>
</footer>
</form>