[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:
Nicolas Pierre
2021-05-31 13:15:54 +00:00
committed by Arnold Moyaux
parent 89c9727020
commit 262b49d85a
17 changed files with 311 additions and 139 deletions
@@ -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)]}"/>
+3
View File
@@ -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',
+99 -9
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
84 access_stock_storage_category_capacity_user stock.storage.category.capacity.user model_stock_storage_category_capacity base.group_user 1 0 0 0
85 access_stock_storage_category_capacity_manager stock.storage.category.capacity.manager model_stock_storage_category_capacity stock.group_stock_manager 1 1 1 1
86 access_stock_inventory_conflict stock.inventory.conflict model_stock_inventory_conflict stock.group_stock_manager 1 1 1 0
87 access_stock_inventory_warning stock.inventory.warning model_stock_inventory_warning stock.group_stock_manager 1 1 1 0
88 access_stock_inventory_adjustment_name stock.inventory.adjustment.name model_stock_inventory_adjustment_name stock.group_stock_manager 1 1 1 0
89 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>
+1 -1
View File
@@ -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)
+17 -11
View File
@@ -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', '&lt;', 0.0)]"/>
<filter name="positive" string="Positive Stock" domain="[('quantity', '&gt;', 0.0)]"/>
<filter name="reserved" string="Reservations" domain="[('reserved_quantity', '&gt;', 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 &lt; 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 &lt; 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 &lt; 0" decoration-success="inventory_diff_quantity &gt; 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 &lt; 0" decoration-success="inventory_diff_quantity &gt; 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>
+2
View File
@@ -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>
+10 -14
View File
@@ -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>