Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FW][FIX] stock: add automated way to fix unreserve issue #156035

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
111 changes: 111 additions & 0 deletions addons/stock/data/stock_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,117 @@
<field name="name">Closest Location</field>
<field name="method">closest</field>
</record>
<record id="stock_quant_stock_move_line_desynchronization" model="ir.actions.server">
<field name="name">Correct inconsistencies for reservation</field>
<field name="model_id" ref="base.model_ir_actions_server"/>
<field name="state">code</field>
<field name="code">
MoveLines = env['stock.move.line'].sudo()
Quants = env['stock.quant'].sudo()
tracked_fields = ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id']
impacted_quants, impacted_move_lines = set(), set()
all_errors = {}

# Compute rounding precision for each UOM
uom_roundings = {
u['id']: u['rounding'] for u in env['uom.uom'].sudo().with_context(active_test=False).search_read([], ['rounding'])
}
products_rounding = {
p['id']: uom_roundings[p['uom_id'][0]]
for p in env['product.product'].sudo().with_context(active_test=False).search_read([], ['uom_id'])
}

# Move that bypass reservations
ignore_moves = env['stock.move'].sudo().search([('state', 'not in', ['draft', 'cancel', 'done'])])
ignore_moves = ignore_moves.filtered(lambda m: m._should_bypass_reservation()).ids

# Get move_lines with incorrect reservation (negative or invalid on state)
incorrect_lines_grouped = MoveLines.read_group(
[
'|',
('reserved_qty', '&lt;', 0),
'&amp;',
('reserved_qty', '!=', 0),
'|',
('state', 'in', ['done', 'draft', 'cancel']),
('move_id', '=', False),
],
tracked_fields + ['ids:array_agg(id)', 'reserved_qty:sum'],
tracked_fields,
lazy=False,
)
for lines_grp in incorrect_lines_grouped:
rd = products_rounding[lines_grp['product_id'][0]]
if float_compare(0, lines_grp['reserved_qty'], precision_rounding=rd) != 0:
impacted_move_lines.update(lines_grp['ids'])

# Get key to match between quants and sml
def get_key(res):
return res['product_id'], res['location_id'], res['lot_id'], res['package_id'], res['owner_id']

# Create a python dictionary containing all quants with reserved quantities in the following format:
# (product_id, location_id, lot_id, package_id, owner_id): (id, reserved_quantity, quantity, rounding)
all_quants = {
get_key(q): (q['id'], q['reserved_quantity'], q['quantity'], products_rounding[q['product_id'][0]])
for q in Quants.search_read([('reserved_quantity', '!=', 0)], tracked_fields + ['reserved_quantity', 'quantity'])
}

# Get all move_lines with reserved quantities
all_grouped_move_lines = MoveLines.read_group(
[
('move_id', 'not in', ignore_moves),
('state', 'not in', ['done', 'draft', 'cancel']),
('reserved_qty', '&gt;', 0),
('move_id', '!=', False),
],
tracked_fields + ['ids:array_agg(id)', 'reserved_qty:sum'],
tracked_fields,
lazy=False,
)

def check_quant(quant_key, quant_val=None, lines=None):
if quant_val is None and lines is None:
return
if quant_val is None:
quant_val = (None, 0, 0, products_rounding[quant_key[0][0]])
if lines is None:
lines = {'ids': [], 'reserved_qty': 0}

quant_id, quant_res, quant_qty, rounding = quant_val
err = False
# Quant reserved must be inferior or equal to the Quant quantity (Before Odoo 17)
err |= float_compare(quant_qty, quant_res, precision_rounding=rounding) == -1
# Quant reserved must be higher or equal to 0
err |= float_compare(0, quant_res, precision_rounding=rounding) == 1
# Quant reserved must be equal to Move reserved
err |= float_compare(lines['reserved_qty'], quant_res, precision_rounding=rounding) != 0
if err:
impacted_move_lines.update(lines['ids'])
if quant_id:
impacted_quants.add(quant_id)

# Check errors on Move Lines and Quants
for lines_grp in all_grouped_move_lines:
sq_key = get_key(lines_grp)
check_quant(sq_key, quant_val=all_quants.pop(sq_key, None), lines=lines_grp)
# Quants with reservation without move lines reserved on it
for sq_key, sq_val in all_quants.items():
check_quant(sq_key, quant_val=sq_val, lines=None)

if impacted_quants:
Quants.browse(impacted_quants).write({'reserved_quantity': 0})
if impacted_move_lines:
lines = MoveLines.browse(impacted_move_lines)
lines.with_context(bypass_reservation_update=True).write({'reserved_uom_qty': 0})
lines.move_id._recompute_state()
if impacted_quants or impacted_move_lines:
report = "Reserved quantity set to 0 for the following: \n- stock.quant: {}\n- stock.move.line: {}".format(
impacted_quants, impacted_move_lines
)
log(report, level="debug")
action = {'type': 'ir.actions.client', 'tag': 'reload'}
</field>
</record>
</data>
<data noupdate="1">
<!-- Resource: stock.location -->
Expand Down
14 changes: 13 additions & 1 deletion addons/stock/i18n/stock.pot
Original file line number Diff line number Diff line change
Expand Up @@ -3742,7 +3742,19 @@ msgstr ""
#: code:addons/stock/models/stock_quant.py:0
#, python-format
msgid ""
"It is not possible to unreserve more products of %s than you have in stock."
"It is not possible to reserve more products of %s than you have in stock.\n\n"
"You can fix the discrepancies by clicking on the button below.\n"
"The correction will remove the reservation of the impacted operations on all companies.\n"
"If the error persists, or you see this message appear often, "
"please submit a Support Ticket at https://www.odoo.com/help"
msgstr ""

#. module: stock
#: code:addons/stock/models/stock_quant.py:0
#, python-format
msgid ""
"It is not possible to unreserve more products of %s than you have in stock.\n"
"Please contact your system administrator to rectify this issue."
msgstr ""

#. module: stock
Expand Down
22 changes: 20 additions & 2 deletions addons/stock/models/stock_quant.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from psycopg2 import Error

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import RedirectWarning, UserError, ValidationError
from odoo.osv import expression
from odoo.tools import check_barcode_encoding, groupby
from odoo.tools.float_utils import float_compare, float_is_zero
Expand Down Expand Up @@ -808,6 +808,19 @@ def _update_available_quantity(self, product_id, location_id, quantity, lot_id=N
})
return self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=False, allow_negative=True), in_date

def _raise_fix_unreserve_action(self, product_id):
action = self.env.ref('stock.stock_quant_stock_move_line_desynchronization', raise_if_not_found=False)
if action and self.user_has_groups('base.group_system'):
msg = _(
'It is not possible to reserve more products of %s than you have in stock.\n\n'
'You can fix the discrepancies by clicking on the button below.\n'
'The correction will remove the reservation of the impacted operations on all companies.\n'
'If the error persists, or you see this message appear often, '
'please submit a Support Ticket at https://www.odoo.com/help',
product_id.display_name
)
raise RedirectWarning(msg, action.id, _('Fix discrepancies'))

@api.model
def _update_reserved_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, strict=False):
""" Increase the reserved quantity, i.e. increase `reserved_quantity` for the set of quants
Expand Down Expand Up @@ -835,7 +848,12 @@ def _update_reserved_quantity(self, product_id, location_id, quantity, lot_id=No
# if we want to unreserve
available_quantity = sum(quants.mapped('reserved_quantity'))
if float_compare(abs(quantity), available_quantity, precision_rounding=rounding) > 0:
raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.', product_id.display_name))
self._raise_fix_unreserve_action(product_id)
raise UserError(_(
'It is not possible to unreserve more products of %s than you have in stock.\n'
'Please contact your system administrator to rectify this issue.',
product_id.display_name
))
else:
return reserved_quants

Expand Down
6 changes: 3 additions & 3 deletions addons/stock/tests/test_quant.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.exceptions import ValidationError
from odoo.tests.common import Form, TransactionCase
from odoo.exceptions import AccessError, UserError
from odoo.exceptions import AccessError, RedirectWarning, UserError


class StockQuant(TransactionCase):
Expand Down Expand Up @@ -440,7 +440,7 @@ def test_increase_decrease_reserved_quantity_1(self):
with self.assertRaises(UserError):
self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
with self.assertRaises(UserError):
with self.assertRaises(RedirectWarning):
self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, -1.0, strict=True)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)

Expand Down Expand Up @@ -485,7 +485,7 @@ def test_mix_tracked_untracked_1(self):
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0)

with self.assertRaises(UserError):
with self.assertRaises(RedirectWarning):
self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, -1.0, strict=True)

self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
Expand Down
55 changes: 54 additions & 1 deletion addons/stock/tests/test_robustness.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import RedirectWarning, UserError, ValidationError
from odoo.tests.common import TransactionCase


Expand Down Expand Up @@ -260,3 +260,56 @@ def test_lot_quantity_remains_unchanged_after_done(self):
moveA._set_lot_ids()

self.assertEqual(moveA.quantity_done, 5)

def test_unreserve_error(self):
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 5)

move = self.env['stock.move'].create({
'name': 'test_lot_id_product_id_mix_move_1',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 5.0,
})
move._action_confirm()
move._action_assign()
quant = self.env['stock.quant']._gather(
self.product1, self.stock_location, strict=True)
quant.sudo().write({"reserved_quantity": 0})
server_action = self.env.ref(
'stock.stock_quant_stock_move_line_desynchronization', raise_if_not_found=False)
if server_action:
with self.assertRaises(RedirectWarning):
move._do_unreserve()
else:
with self.assertRaises(UserError):
move._do_unreserve()

def test_unreserve_fix(self):
self.env['stock.quant']._update_available_quantity(
self.product1, self.stock_location, 5)

move = self.env['stock.move'].create({
'name': 'test_lot_id_product_id_mix_move_1',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 5.0,
})

move._action_confirm()
move._action_assign()

quant = self.env['stock.quant']._gather(
self.product1, self.stock_location, strict=True)
quant.sudo().write({"reserved_quantity": 0})
server_action = self.env.ref(
'stock.stock_quant_stock_move_line_desynchronization', raise_if_not_found=False)
if not server_action:
return
server_action.run()
self.assertEqual(move.reserved_availability, 0)
self.assertEqual(move.state, 'confirmed')
self.assertEqual(quant.reserved_quantity, 0)