[IMP] stock: Inventory adjustments improvements
General improvements of the new Inventory Adjustment mechanism, among which: From the Inventory Adjustment page - A new inventory_quantity_set Boolean field is used to hide the Counted Quantity when not set instead of showing 0. - Possibile to set 0 as a Counted Quantity directly (not using the set button). - Allows to provide a name for the Inventory Adjustment when using the Apply button at the top of the tree view. This name is applied on the corresponding stock moves. - Add warnings when trying to Apply/Set on multiple records when some records are not set/already set. - Highlight lines when there is a mismatch between Quantity / Counted Quantity / Difference. If the user still tries to Apply, shows the option to either keep the current Difference or the current Counted Quantity for all records. - Set the user when directly inserting a Counted Quantity (before only when using the Set button) - Add the possibility to import records. - Add "To Apply" filter. - Add a 'Reset' button at the top of the tree view for multiple records. From the Update Quantity page of a product - Remove the "Updated on Hand Quantity" and can directly change "On Hand" quantity - Pre-fill a default location (Last used location for tracked products or default destination location in single WH environment) From the Inventory Report - Remove the "Updated on Hand Quantity" and can directly change "On Hand" quantity closes odoo/odoo#71199 Task-id: 2495664 Pr-community: 71199 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
This commit is contained in:
committed by
Arnold Moyaux
parent
89c9727020
commit
262b49d85a
@@ -32,7 +32,7 @@
|
||||
<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']" position="before">
|
||||
<xpath expr="//field[@name='inventory_quantity_auto_apply']" position="before">
|
||||
<field name="use_expiration_date" invisible="1"/>
|
||||
<field name="removal_date" optional="show"
|
||||
invisible="context.get('hide_removal_date')" attrs="{'readonly': ['|', ('tracking', '=', 'none'), ('use_expiration_date', '=', False)]}"/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -9,18 +9,4 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-extend="StockInventoryReport.Buttons" t-name="StockQuant.Buttons">
|
||||
<t t-jquery="button.o_button_at_date" t-operation="after">
|
||||
<button class="btn btn-primary d-none o_button_apply_inventory" type="button">
|
||||
Apply
|
||||
</button>
|
||||
<button class="btn btn-primary d-none o_button_set_inventory" type="button">
|
||||
Set
|
||||
</button>
|
||||
<button class="btn btn-primary d-none o_button_request_count_inventory" type="button">
|
||||
Request a Count
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
<filter name='internal_loc' string="Internal Locations" domain="[('location_id.usage','=', 'internal')]"/>
|
||||
<filter name='transit_loc' string="Transit Locations" domain="[('location_id.usage' ,'=', 'transit')]"/>
|
||||
<filter name="on_hand" string="On Hand" domain="[('on_hand', '=', True)]"/>
|
||||
<filter name="to_apply" string="To Apply" domain="[('inventory_quantity_set', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter name="negative" string="Negative Stock" domain="[('quantity', '<', 0.0)]"/>
|
||||
<filter name="positive" string="Positive Stock" domain="[('quantity', '>', 0.0)]"/>
|
||||
<filter name="reserved" string="Reservations" domain="[('reserved_quantity', '>', 0.0)]"/>
|
||||
<separator/>
|
||||
<filter name="filter_in_date" date="in_date"/>
|
||||
@@ -143,9 +143,7 @@
|
||||
<field name="owner_id" groups="stock.group_tracking_owner"
|
||||
attrs="{'readonly': [('id', '!=', False)]}"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="inventory_quantity" string="Updated On Hand Quantity" decoration-danger="inventory_quantity < 0"
|
||||
readonly="0"/>
|
||||
<field name="quantity" string="On Hand Quantity"/>
|
||||
<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"/>
|
||||
@@ -340,9 +338,17 @@
|
||||
<field name="model">stock.quant</field>
|
||||
<field eval="10" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree default_order="location_id, inventory_date, product_id, package_id, lot_id, owner_id" editable="bottom" create="1" edit="1" import="0" js_class="singleton_list" sample="1">
|
||||
<tree default_order="location_id, inventory_date, product_id, package_id, lot_id, owner_id" decoration-warning='is_outdated' editable="bottom" create="1" edit="1" import="0" js_class="singleton_list" sample="1">
|
||||
<header>
|
||||
<button name="stock.action_stock_inventory_adjustement_name" groups="stock.group_stock_manager" type="action" string="Apply"/>
|
||||
<button name="action_set_inventory_quantity" type="object" string="Set"/>
|
||||
<button name="action_reset" type="object" string="Clear"/>
|
||||
<button name="stock.action_stock_request_count" groups="stock.group_stock_manager" type="action" string="Request a Count"/>
|
||||
</header>
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="is_outdated" 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="product_id" attrs="{'readonly': [('id', '!=', False)]}" readonly="context.get('single_product', False)" force_save="1" options="{'no_create': True}"/>
|
||||
<field name="lot_id" groups="stock.group_production_lot"
|
||||
@@ -354,15 +360,15 @@
|
||||
<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="product_uom_id" groups="uom.group_uom" string="UoM"/>
|
||||
<field name="inventory_quantity" decoration-muted="inventory_quantity == 0"/>
|
||||
<field name="inventory_diff_quantity" string="Difference" decoration-muted="inventory_quantity == 0" decoration-danger="inventory_diff_quantity < 0" decoration-success="inventory_diff_quantity > 0" decoration-bf="inventory_diff_quantity != 0"/>
|
||||
<field name="inventory_quantity" widget="counted_quantity_widget"/>
|
||||
<field name="inventory_diff_quantity" string="Difference" attrs="{'invisible': [('inventory_quantity_set', '=', False)]}" decoration-muted="inventory_quantity == 0" decoration-danger="inventory_diff_quantity < 0" decoration-success="inventory_diff_quantity > 0" decoration-bf="inventory_diff_quantity != 0"/>
|
||||
<field name="inventory_date" optional="show"/>
|
||||
<field name="user_id" string="User" optional="show"/>
|
||||
<field name='company_id' groups="base.group_multi_company" optional="hide"/>
|
||||
<button name="action_apply_inventory" groups="stock.group_stock_manager" type="object" string="Apply" class="btn btn-link" icon="fa-save" attrs="{'invisible': [('inventory_diff_quantity', '=', 0), ('inventory_quantity', '=', 0), ('quantity', '!=', 0)]}"/>
|
||||
<button name="action_set_inventory_quantity" type="object" string="Set" class="btn btn-link" icon="fa-bullseye" attrs="{'invisible': ['|', '|', ('quantity', '=', 0.0), ('inventory_diff_quantity', '!=', 0), ('inventory_quantity', '!=', 0)]}"/>
|
||||
<button name="action_inventory_history" type="object" class="btn btn-link" icon="fa-history" string="History"/>
|
||||
<button name="action_set_inventory_quantity_to_zero" type="object" string="Reset" class="btn" icon="fa-times" attrs="{'invisible': [('inventory_diff_quantity', '=', 0), ('inventory_quantity', '=', 0), ('quantity', '!=', 0)]}"/>
|
||||
<button name="action_inventory_history" type="object" class="btn btn-link text-info" icon="fa-history" string="History"/>
|
||||
<button name="action_apply_inventory" groups="stock.group_stock_manager" type="object" string="Apply" class="btn btn-link text-success" icon="fa-save" attrs="{'invisible': [('inventory_quantity_set', '=', False)]}"/>
|
||||
<button name="action_set_inventory_quantity" type="object" string="Set" class="btn btn-link" icon="fa-bullseye" attrs="{'invisible': [('inventory_quantity_set', '=', True)]}"/>
|
||||
<button name="action_set_inventory_quantity_to_zero" type="object" string="Clear" class="btn text-warning" icon="fa-times" attrs="{'invisible': [('inventory_quantity_set', '=', False)]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -5,6 +5,8 @@ from . import stock_assign_serial_numbers
|
||||
from . import stock_picking_return
|
||||
from . import stock_change_product_qty
|
||||
from . import stock_inventory_conflict
|
||||
from . import stock_inventory_warning
|
||||
from . import stock_inventory_adjustment_name
|
||||
from . import stock_scheduler_compute
|
||||
from . import stock_immediate_transfer
|
||||
from . import stock_backorder_confirmation
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class StockInventoryAdjustmentName(models.TransientModel):
|
||||
_name = 'stock.inventory.adjustment.name'
|
||||
_description = 'Inventory Adjustment Name'
|
||||
|
||||
def _default_inventory_adjustment_name(self):
|
||||
return _("Inventory Adjustment") + " - " + fields.Date.to_string(fields.Date.today())
|
||||
|
||||
quant_ids = fields.Many2many('stock.quant')
|
||||
inventory_adjustment_name = fields.Char(default=_default_inventory_adjustment_name)
|
||||
|
||||
def action_apply(self):
|
||||
return self.quant_ids.with_context(
|
||||
inventory_name=self.inventory_adjustment_name).action_apply_inventory()
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="stock_inventory_adjustment_name_form_view" model="ir.ui.view">
|
||||
<field name="name">stock.inventory.adjustment.name.form.view</field>
|
||||
<field name="model">stock.inventory.adjustment.name</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div>
|
||||
<group>
|
||||
<field name="inventory_adjustment_name" string="Inventory Reference"/>
|
||||
</group>
|
||||
</div>
|
||||
<footer>
|
||||
<button name="action_apply" string="Apply" type="object" class="btn-primary"/>
|
||||
<button name="cancel_button" string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_stock_inventory_adjustement_name" model="ir.actions.act_window">
|
||||
<field name="name">Name Inventory Adjustement</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">stock.inventory.adjustment.name</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="stock_inventory_adjustment_name_form_view"/>
|
||||
<field name="context">{
|
||||
'default_quant_ids': active_ids
|
||||
}</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,8 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockInventoryConflict(models.TransientModel):
|
||||
@@ -14,15 +13,12 @@ class StockInventoryConflict(models.TransientModel):
|
||||
quant_to_fix_ids = fields.Many2many(
|
||||
'stock.quant', string='Conflicts')
|
||||
|
||||
def action_validate(self):
|
||||
for conflict in self:
|
||||
quant_to_fix_ids = conflict.quant_to_fix_ids.filtered(
|
||||
lambda q: q.quantity != (q.inventory_quantity - q.inventory_diff_quantity))
|
||||
if quant_to_fix_ids:
|
||||
if self.user_has_groups('stock.group_stock_multi_locations'):
|
||||
message = '\n'.join([_('%s at location %s', q.product_id.display_name, q.location_id.display_name) for q in quant_to_fix_ids])
|
||||
else:
|
||||
message = '\n'.join([_('%s', q.product_id.display_name) for q in quant_to_fix_ids])
|
||||
raise UserError(
|
||||
_('You still have conflicts to resolve:\n') + message)
|
||||
conflict.quant_ids.action_apply_inventory()
|
||||
def action_keep_counted_quantity(self):
|
||||
for quant in self.quant_ids:
|
||||
quant.inventory_diff_quantity = quant.inventory_quantity - quant.quantity
|
||||
return self.quant_ids.action_apply_inventory()
|
||||
|
||||
def action_keep_difference(self):
|
||||
for quant in self.quant_ids:
|
||||
quant.inventory_quantity = quant.quantity + quant.inventory_diff_quantity
|
||||
return self.quant_ids.action_apply_inventory()
|
||||
|
||||
@@ -7,12 +7,20 @@
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div>
|
||||
<strong>Some moves were made since your last inventory count. Plese count the inventory quantity again.</strong>
|
||||
<strong>Due to some stock moves done between your initial update of the quantity and now, the difference of quantity is not consistent anymore.</strong>
|
||||
</div>
|
||||
<div>
|
||||
You can either :
|
||||
<ul>
|
||||
<li>Keep the <strong>Counted Quantity</strong> (the Difference will be updated)</li>
|
||||
<li>Keep the <strong>Difference</strong> (the Counted Quantity will be updated to reflect the same difference as when you counted)</li>
|
||||
<li>Discard and manually resolve the conflict</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<br/>
|
||||
<field name="quant_ids" invisible="1"/>
|
||||
<field name="quant_to_fix_ids" style="margin-top:10px;">
|
||||
<field name="quant_to_fix_ids" readonly="1" style="margin-top:10px;">
|
||||
<tree editable="bottom" create="0" delete="0">
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="tracking" invisible="1"/>
|
||||
@@ -33,7 +41,8 @@
|
||||
</field>
|
||||
</div>
|
||||
<footer>
|
||||
<button name="action_validate" string="Confirm" type="object" class="btn-primary"/>
|
||||
<button name="action_keep_counted_quantity" string="Keep Counted Quantity" type="object" class="btn-primary"/>
|
||||
<button name="action_keep_difference" string="Keep Difference" type="object" class="btn-primary"/>
|
||||
<button name="cancel_button" string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockInventoryWarning(models.TransientModel):
|
||||
_name = 'stock.inventory.warning'
|
||||
_description = 'Inventory Adjustment Warning'
|
||||
|
||||
quant_ids = fields.Many2many('stock.quant')
|
||||
|
||||
def action_apply(self):
|
||||
return self.quant_ids.filtered(lambda q: q.inventory_quantity_set).action_apply_inventory()
|
||||
|
||||
def action_reset(self):
|
||||
return self.quant_ids.action_set_inventory_quantity_to_zero()
|
||||
|
||||
def action_set(self):
|
||||
valid_quants = self.quant_ids.filtered(lambda quant: not quant.inventory_quantity_set)
|
||||
return valid_quants.action_set_inventory_quantity()
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="inventory_warning_reset_view" model="ir.ui.view">
|
||||
<field name="name">inventory.reset.warning.view</field>
|
||||
<field name="model">stock.inventory.warning</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div>
|
||||
This will discard all unapplied counts, do you want to proceed?
|
||||
</div>
|
||||
<footer>
|
||||
<button name="action_reset" string="Continue" type="object" class="btn-primary"/>
|
||||
<button name="cancel_button" string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="inventory_warning_set_view" model="ir.ui.view">
|
||||
<field name="name">inventory.set.warning.view</field>
|
||||
<field name="model">stock.inventory.warning</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div>
|
||||
Some selected lines already have quantities set, they will be ignored.
|
||||
</div>
|
||||
<footer>
|
||||
<button name="action_set" string="Continue" type="object" class="btn-primary"/>
|
||||
<button name="cancel_button" string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="inventory_warning_apply_view" model="ir.ui.view">
|
||||
<field name="name">inventory.apply.warning.view</field>
|
||||
<field name="model">stock.inventory.warning</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div>
|
||||
Some selected lines don't have any quantities set, they will be ignored.
|
||||
</div>
|
||||
<footer>
|
||||
<button name="action_apply" string="Continue" type="object" class="btn-primary"/>
|
||||
<button name="cancel_button" string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -25,6 +25,9 @@
|
||||
<field name="res_model">stock.request.count</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="stock_inventory_request_count_form_view"/>
|
||||
<field name="context">{
|
||||
'default_quant_ids': active_ids
|
||||
}</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user