[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:
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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)]}"/>
|
||||
|
||||
@@ -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', '<=', 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', '<', 0.0)]"/>
|
||||
<filter name="reserved" string="Reservations" domain="[('reserved_quantity', '>', 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']), '&', ('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 < 0" optional="hidden"/>
|
||||
<field name="quantity" optional="show" string="On Hand Quantity"/>
|
||||
<field name="quantity" optional="show" decoration-warning="quantity < 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 < 0" decoration-success="inventory_diff_quantity > 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>
|
||||
|
||||
Reference in New Issue
Block a user