diff --git a/addons/product_expiry/views/stock_quant_views.xml b/addons/product_expiry/views/stock_quant_views.xml index d15b1437b2c..0bb23660d7c 100644 --- a/addons/product_expiry/views/stock_quant_views.xml +++ b/addons/product_expiry/views/stock_quant_views.xml @@ -32,7 +32,7 @@ stock.quant - + diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py index 9d9b848bf01..90f895593e9 100644 --- a/addons/stock/__manifest__.py +++ b/addons/stock/__manifest__.py @@ -60,6 +60,8 @@ 'wizard/stock_track_confirmation_views.xml', 'wizard/stock_orderpoint_snooze_views.xml', 'wizard/stock_package_destination_views.xml', + 'wizard/stock_inventory_adjustment_name.xml', + 'wizard/stock_inventory_warning.xml', 'views/res_partner_views.xml', 'views/product_strategy_views.xml', @@ -110,6 +112,7 @@ '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/tests/tours/stock_report_tests.js', diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 68404203147..70fd13db007 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -97,6 +97,10 @@ class StockQuant(models.Model): inventory_quantity = fields.Float( 'Counted Quantity', digits='Product Unit of Measure', help="The product's counted quantity.") + inventory_quantity_auto_apply = fields.Float( + 'Inventoried Quantity', compute='_compute_inventory_quantity_auto_apply', + inverse='_set_inventory_quantity', groups='stock.group_stock_manager' + ) inventory_diff_quantity = fields.Float( 'Difference', compute='_compute_inventory_diff_quantity', store=True, help="Indicates the gap between the product's theoretical quantity and its counted quantity.", @@ -104,6 +108,8 @@ 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.") + inventory_quantity_set = fields.Boolean() # Only used for UI purposes in the Inventory Adjustment page + is_outdated = fields.Boolean('Quantity has been moved since last count', compute='_compute_is_outdated') user_id = fields.Many2one( 'res.users', 'Assigned To', help="User assigned to do product count.") @@ -122,8 +128,31 @@ class StockQuant(models.Model): @api.depends('inventory_quantity') def _compute_inventory_diff_quantity(self): for quant in self: + quant.inventory_quantity_set = True quant.inventory_diff_quantity = quant.inventory_quantity - quant.quantity + @api.depends('inventory_quantity', 'quantity', 'product_id') + def _compute_is_outdated(self): + self.is_outdated = False + for quant in self: + if quant.product_id and float_compare(quant.inventory_quantity - quant.inventory_diff_quantity, quant.quantity, precision_rounding=quant.product_uom_id.rounding) and quant.inventory_quantity_set: + quant.is_outdated = True + + @api.depends('quantity') + def _compute_inventory_quantity_auto_apply(self): + for quant in self: + quant.inventory_quantity_auto_apply = quant.quantity + + def _set_inventory_quantity(self): + """ Inverse method to create stock move when `inventory_quantity` is set + (`inventory_quantity` is only accessible in inventory mode). + """ + if not self._is_inventory_mode(): + return + for quant in self: + quant.inventory_quantity = quant.inventory_quantity_auto_apply + self.action_apply_inventory() + def _search_on_hand(self, operator, value): """Handle the "on_hand" filter, indirectly calling `_get_domain_locations`.""" if operator not in ['=', '!='] or not isinstance(value, bool): @@ -141,12 +170,13 @@ class StockQuant(models.Model): """ Override to handle the "inventory mode" and create a quant as superuser the conditions are met. """ - if self._is_inventory_mode() and 'inventory_quantity' in vals: + if self._is_inventory_mode() and any(f in vals for f in ['inventory_quantity', 'inventory_quantity_auto_apply']): allowed_fields = self._get_inventory_fields_create() if any(field for field in vals.keys() if field not in allowed_fields): raise UserError(_("Quant's creation is restricted, you can't do this operation.")) - inventory_quantity = vals.pop('inventory_quantity') + inventory_quantity = vals.pop('inventory_quantity', False) or vals.pop( + 'inventory_quantity_auto_apply', False) or 0 # Create an empty quant or write on a similar one. product = self.env['product.product'].browse(vals['product_id']) location = self.env['stock.location'].browse(vals['location_id']) @@ -154,8 +184,9 @@ class StockQuant(models.Model): package_id = self.env['stock.quant.package'].browse(vals.get('package_id')) owner_id = self.env['res.partner'].browse(vals.get('owner_id')) quant = self._gather(product, location, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) + if quant: - quant = quant[0] + quant = quant[0].sudo() else: quant = self.sudo().create(vals) # Set the `inventory_quantity` field to create the necessary move. @@ -256,7 +287,6 @@ class StockQuant(models.Model): return action def action_apply_inventory(self): - quants_outdated = [] products_tracked_without_lot = [] for quant in self: rounding = quant.product_uom_id.rounding @@ -264,16 +294,28 @@ class StockQuant(models.Model): and fields.Float.is_zero(quant.inventory_quantity, precision_rounding=rounding)\ and fields.Float.is_zero(quant.quantity, precision_rounding=rounding): continue - if fields.Float.compare(quant.inventory_quantity - quant.inventory_diff_quantity, quant.quantity, precision_rounding=rounding): - quants_outdated.append(quant.id) if quant.product_id.tracking in ['lot', 'serial'] and\ not quant.lot_id and quant.inventory_quantity != 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 {}) ctx['default_quant_ids'] = self.ids + quants_not_entered = self.filtered(lambda quant: not quant.inventory_quantity_set) + if quants_not_entered: + view = self.env.ref('stock.inventory_warning_apply_view', False) + return { + 'name': _('Quantities Not Entered'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'views': [(view.id, 'form')], + 'view_id': view.id, + 'res_model': 'stock.inventory.warning', + 'target': 'new', + 'context': ctx, + } + quants_outdated = self.filtered(lambda quant: quant.is_outdated) if quants_outdated: - ctx['default_quant_to_fix_ids'] = quants_outdated + ctx['default_quant_to_fix_ids'] = quants_outdated.ids return { 'name': _('Conflict in Inventory Adjustment'), 'type': 'ir.actions.act_window', @@ -295,6 +337,7 @@ class StockQuant(models.Model): 'context': ctx, } self._apply_inventory() + self.inventory_quantity_set = False def action_inventory_history(self): self.ensure_one() @@ -326,13 +369,43 @@ class StockQuant(models.Model): return action def action_set_inventory_quantity(self): + quants_already_set = self.filtered(lambda quant: quant.inventory_quantity_set) + if quants_already_set: + ctx = dict(self.env.context or {}, default_quant_ids=self.ids) + view = self.env.ref('stock.inventory_warning_set_view', False) + return { + 'name': _('Quantities Already Set'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'views': [(view.id, 'form')], + 'view_id': view.id, + 'res_model': 'stock.inventory.warning', + 'target': 'new', + 'context': ctx, + } for quant in self: quant.inventory_quantity = quant.quantity self.user_id = self.env.user.id + self.inventory_quantity_set = True + + def action_reset(self): + ctx = dict(self.env.context or {}, default_quant_ids=self.ids) + view = self.env.ref('stock.inventory_warning_reset_view', False) + return { + 'name': _('Quantities To Reset'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'views': [(view.id, 'form')], + 'view_id': view.id, + 'res_model': 'stock.inventory.warning', + 'target': 'new', + 'context': ctx, + } def action_set_inventory_quantity_to_zero(self): self.inventory_quantity = 0 self.inventory_diff_quantity = 0 + self.inventory_quantity_set = False @api.constrains('product_id') def check_product_id(self): @@ -453,6 +526,7 @@ class StockQuant(models.Model): # it'll trigger `inventory_quantity` compute. if self.lot_id and self.tracking == 'serial': vals['inventory_quantity'] = 1 + vals['inventory_quantity_auto_apply'] = 1 if vals: self.update(vals) @@ -478,6 +552,20 @@ class StockQuant(models.Model): if message: return {'warning': {'title': _('Warning'), 'message': message}} + @api.onchange('product_id', 'company_id') + def _onchange_product_id(self): + if self.location_id: + return + if self.product_id.tracking in ['lot', 'serial']: + previous_quants = self.env['stock.quant'].search( + [('product_id', '=', self.product_id.id)], limit=1, order='create_date desc') + if previous_quants: + self.location_id = previous_quants.location_id + if not self.location_id: + company_id = self.company_id and self.company_id.id or self.env.company.id + self.location_id = self.env['stock.warehouse'].search( + [('company_id', '=', company_id)], limit=1).in_type_id.default_location_dest_id + def _apply_inventory(self): move_vals = [] if not self.user_has_groups('stock.group_stock_manager'): @@ -693,7 +781,9 @@ class StockQuant(models.Model): def _get_inventory_fields_write(self): """ Returns a list of fields user can edit when he want to edit a quant in `inventory_mode`. """ - return ['inventory_quantity', 'inventory_diff_quantity', 'inventory_date', 'user_id'] + fields = ['inventory_quantity', 'inventory_quantity_auto_apply', 'inventory_diff_quantity', + 'inventory_date', 'user_id', 'inventory_quantity_set', 'is_outdated'] + return fields def _get_inventory_move_values(self, qty, location_id, location_dest_id, out=False): """ Called when user manually set a new quantity (via `inventory_quantity`) @@ -710,7 +800,7 @@ class StockQuant(models.Model): else: name = _('Product Quantity Updated') return { - 'name': name, + 'name': self.env.context.get('inventory_name') or name, 'product_id': self.product_id.id, 'product_uom': self.product_uom_id.id, 'product_uom_qty': qty, diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv index 40489e6a6ca..ad6b4a04aaa 100644 --- a/addons/stock/security/ir.model.access.csv +++ b/addons/stock/security/ir.model.access.csv @@ -84,4 +84,6 @@ access_stock_storage_category_manager,stock.storage.category.manager,model_stock access_stock_storage_category_capacity_user,stock.storage.category.capacity.user,model_stock_storage_category_capacity,base.group_user,1,0,0,0 access_stock_storage_category_capacity_manager,stock.storage.category.capacity.manager,model_stock_storage_category_capacity,stock.group_stock_manager,1,1,1,1 access_stock_inventory_conflict,stock.inventory.conflict,model_stock_inventory_conflict,stock.group_stock_manager,1,1,1,0 +access_stock_inventory_warning,stock.inventory.warning,model_stock_inventory_warning,stock.group_stock_manager,1,1,1,0 +access_stock_inventory_adjustment_name,stock.inventory.adjustment.name,model_stock_inventory_adjustment_name,stock.group_stock_manager,1,1,1,0 access_stock_request_count,stock.request.count,model_stock_request_count,stock.group_stock_manager,1,1,1,0 diff --git a/addons/stock/static/src/js/counted_quantity_widget.js b/addons/stock/static/src/js/counted_quantity_widget.js new file mode 100644 index 00000000000..3a2175b1f4f --- /dev/null +++ b/addons/stock/static/src/js/counted_quantity_widget.js @@ -0,0 +1,36 @@ +/** @odoo-module alias=stock.counted_quantity_widget **/ + +import BasicFields from 'web.basic_fields'; +import fieldRegistry from 'web.field_registry'; + +const CountedQuantityWidgetField = BasicFields.FieldFloat.extend({ + supportedFieldTypes: ['float'], + + _renderReadonly: function () { + if (this.recordData.inventory_quantity_set) { + this.el.textContent = this._formatValue(this.recordData.inventory_quantity); + } else { + this.el.textContent = ""; + } + }, + + _onChange: function () { + if (!this.recordData.inventory_quantity_set) { + this.recordData.inventory_quantity_set = true; + } + this._super.apply(this); + }, + + _isSameValue: function(value) { + // We want to trigger the update of the view when inserting 0 + if (value == 0) { + return false; + } + return this._super(...arguments); + } + +}); + +fieldRegistry.add('counted_quantity_widget', CountedQuantityWidgetField); + +export default CountedQuantityWidgetField; diff --git a/addons/stock/static/src/js/inventory_singleton_list_controller.js b/addons/stock/static/src/js/inventory_singleton_list_controller.js index 2f5fdba3679..22dc2ea4b28 100644 --- a/addons/stock/static/src/js/inventory_singleton_list_controller.js +++ b/addons/stock/static/src/js/inventory_singleton_list_controller.js @@ -21,20 +21,6 @@ var _t = core._t; */ var SingletonListController = InventoryReportListController.extend({ - buttons_template: 'StockQuant.Buttons', - - // ------------------------------------------------------------------------- - // Public - // ------------------------------------------------------------------------- - /** - * @override - */ - renderButtons: function ($node) { - this._super.apply(this, arguments); - this.$buttons.on('click', '.o_button_apply_inventory', this._onApplyInventory.bind(this)); - this.$buttons.on('click', '.o_button_request_count_inventory', this._onRequestCountInventory.bind(this)); - this.$buttons.on('click', '.o_button_set_inventory', this._onSetInventory.bind(this)); - }, // ------------------------------------------------------------------------- // Private @@ -85,54 +71,10 @@ var SingletonListController = InventoryReportListController.extend({ } }, - /** - * Override to show/hide inventory adjustment specific buttons. - * Maybe a bit too hacky since it's not the intended purpose of this function, - * but for some reason `_updateControlPanel` isn't called during reload action - * after triggering any of these buttons. - * - * @override - */ - _renderHeaderButtons() { - this._super.apply(this, arguments); - - if (this.selectedRecords.length > 0 && !this.context.inventory_report_mode) { - $('.o_button_apply_inventory').removeClass('d-none'); - $('.o_button_request_count_inventory').removeClass('d-none'); - $('.o_button_set_inventory').removeClass('d-none'); - } else { - $('.o_button_apply_inventory').addClass('d-none'); - $('.o_button_request_count_inventory').addClass('d-none'); - $('.o_button_set_inventory').addClass('d-none'); - } - }, - // ------------------------------------------------------------------------- // Handlers // ------------------------------------------------------------------------- - _onApplyInventory: function (ev) { - var self = this; - var ids = this.getSelectedIds(); - if (!ids.length) { - var modified_records = this.initialState.data.filter(record => record.data.inventory_diff_quantity != 0) - ids = modified_records.map(record => record.data.id) - } - if (ids.length) { - return this._rpc({ - model: 'stock.quant', - method: 'action_apply_inventory', - args: [ids], - context: this.context, - }).then((result) => { - if (!result) { - return self.trigger_up('reload'); - } - return self.do_action(result, {on_close: () => this.reload(),}); - }) - } - }, - /** * * @private @@ -153,34 +95,6 @@ var SingletonListController = InventoryReportListController.extend({ self._enableButtons(); }).guardedCatch(this._enableButtons.bind(this)); }, - - _onRequestCountInventory: function (ev) { - var ids = this.getSelectedIds(); - if ( ids.length ) { - return this.do_action('stock.action_stock_request_count', - { - additional_context: { - default_quant_ids: ids, - }, - on_close: () => this.reload(), - }); - } - }, - - _onSetInventory: function (ev) { - var self = this; - var ids = this.getSelectedIds(); - if (ids.length) { - return this._rpc({ - model: 'stock.quant', - method: 'action_set_inventory_quantity', - args: [ids], - context: this.context, - }).then(() => { - return self.trigger_up('reload'); - }); - } - }, }); return SingletonListController; diff --git a/addons/stock/static/src/xml/inventory_report.xml b/addons/stock/static/src/xml/inventory_report.xml index 757d235ad18..15f60f6fde0 100644 --- a/addons/stock/static/src/xml/inventory_report.xml +++ b/addons/stock/static/src/xml/inventory_report.xml @@ -9,18 +9,4 @@ - - - - - - - - diff --git a/addons/stock/tests/test_inventory.py b/addons/stock/tests/test_inventory.py index 814ce766660..59be98876b5 100644 --- a/addons/stock/tests/test_inventory.py +++ b/addons/stock/tests/test_inventory.py @@ -354,7 +354,7 @@ class TestInventory(TransactionCase): conflict_wizard_form = Form(self.env['stock.inventory.conflict'].with_context(conflict_wizard_values['context'])) conflict_wizard = conflict_wizard_form.save() conflict_wizard.quant_to_fix_ids.inventory_quantity = 5 - conflict_wizard.action_validate() + conflict_wizard.action_keep_counted_quantity() self.assertEqual(inventory_quant.inventory_diff_quantity, 0) self.assertEqual(inventory_quant.inventory_quantity, 0) self.assertEqual(inventory_quant.quantity, 5) diff --git a/addons/stock/views/stock_quant_views.xml b/addons/stock/views/stock_quant_views.xml index 8c4a6a94e4f..4460dd02c57 100644 --- a/addons/stock/views/stock_quant_views.xml +++ b/addons/stock/views/stock_quant_views.xml @@ -18,9 +18,9 @@ + - @@ -143,9 +143,7 @@ - - + @@ -340,9 +338,17 @@ stock.quant - + +
+
+ + - - + + -