[FIX] stock: create/write on stock.move.lineresponssible for quant reservation
A lot of issue with "It is not possible to unreserve more products of ... than you have in stock". It's the result of a desynchronisation between `stock.move.line`.`reserved_qty` and `stock.quant`.`reserved_quantity` fields. It should never happens in theory. However we already faced it a lot due to bugs/custom code/server actions... It's not trivial to fix because the issue come from data corruption. So it is hard to spot the different main issues. In order to avoid it, this commit try to modify the structure of stock module. Before the operations was made in this order: - The `stock.move` checks the quantity available on `stock.quant` - The `stock.move` write the quantity reserved on the `stock.quant` and save it in a variable - The `stock.move` create a `stock.move.line` with the quanity reserved. The main issue is that a `stock.move.line` could be easily created with a reserved quantity while the `stock.quant` are not update nor checked. After this commit the operations will be: - The `stock.move` checks the quantity available - The `stock.move` dispatch the available quantity among the `stock.move.line` based on create or write calls. - The `stock.move.line` reserve the quantity on `stock.quant` - If the quantity is bigger than available, we write the max available on `stock.move.line` The idea is to respect the different layers `stock.move` <-> `stock.move.line` <-> `stock.quant`. Avoid the interactions bewteen `stock.move` and `stock.quant` This behavior is already well managed in other use cases. E.g. the real quantity itself, `_do_unreserve` of `stock.move`,... opw - a lot Part-of: odoo/odoo#115328
This commit is contained in:
@@ -1695,7 +1695,7 @@ class MrpProduction(models.Model):
|
||||
# Unreserve the quantity removed from initial `stock.move.line` and
|
||||
# not assigned to a move anymore. In case of a split smaller than initial
|
||||
# quantity and fully reserved
|
||||
if quantity:
|
||||
if quantity and not move_line.move_id._should_bypass_reservation():
|
||||
self.env['stock.quant']._update_reserved_quantity(
|
||||
move_line.product_id, move_line.location_id, -quantity,
|
||||
lot_id=move_line.lot_id, package_id=move_line.package_id,
|
||||
@@ -1712,7 +1712,7 @@ class MrpProduction(models.Model):
|
||||
# Avoid triggering a useless _recompute_state
|
||||
self.env['stock.move.line'].browse(move_lines_to_unlink).write({'move_id': False})
|
||||
self.env['stock.move.line'].browse(move_lines_to_unlink).unlink()
|
||||
self.env['stock.move.line'].create(move_lines_vals)
|
||||
self.env['stock.move.line'].with_context(bypass_reservation_update=True).create(move_lines_vals)
|
||||
|
||||
workorders_to_cancel = self.env['mrp.workorder']
|
||||
for production in self:
|
||||
@@ -1851,7 +1851,7 @@ class MrpProduction(models.Model):
|
||||
order._check_sn_uniqueness()
|
||||
|
||||
def do_unreserve(self):
|
||||
self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))._do_unreserve()
|
||||
(self.move_finished_ids | self.move_raw_ids).filtered(lambda x: x.state not in ('done', 'cancel'))._do_unreserve()
|
||||
|
||||
def button_scrap(self):
|
||||
self.ensure_one()
|
||||
@@ -2083,6 +2083,8 @@ class MrpProduction(models.Model):
|
||||
if move.has_tracking != 'serial' or move.product_id == self.product_id:
|
||||
continue
|
||||
for move_line in move.move_line_ids:
|
||||
if float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding):
|
||||
continue
|
||||
if self._is_finished_sn_already_produced(move_line.lot_id, excluded_sml=move_line):
|
||||
raise UserError(_('The serial number %(number)s used for byproduct %(product_name)s has already been produced',
|
||||
number=move_line.lot_id.name, product_name=move_line.product_id.name))
|
||||
@@ -2129,6 +2131,8 @@ class MrpProduction(models.Model):
|
||||
raise UserError(message)
|
||||
|
||||
def _is_finished_sn_already_produced(self, lot, excluded_sml=None):
|
||||
if not lot:
|
||||
return False
|
||||
excluded_sml = excluded_sml or self.env['stock.move.line']
|
||||
domain = [
|
||||
('lot_id', '=', lot.id),
|
||||
|
||||
@@ -478,11 +478,6 @@ class StockMove(models.Model):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _should_bypass_reservation(self, forced_location=False):
|
||||
res = super(StockMove, self)._should_bypass_reservation(
|
||||
forced_location=forced_location)
|
||||
return bool(res and not self.production_id)
|
||||
|
||||
def _key_assign_picking(self):
|
||||
keys = super(StockMove, self)._key_assign_picking()
|
||||
return keys + (self.created_production_id,)
|
||||
|
||||
@@ -1005,19 +1005,19 @@ class TestMrpOrder(TestMrpCommon):
|
||||
self.assertEqual(len(move_byproduct_1), 1)
|
||||
self.assertEqual(move_byproduct_1.product_uom_qty, 2.0)
|
||||
self.assertEqual(move_byproduct_1.quantity_done, 0)
|
||||
self.assertEqual(len(move_byproduct_1.move_line_ids), 0)
|
||||
self.assertEqual(len(move_byproduct_1.move_line_ids), 2)
|
||||
|
||||
move_byproduct_2 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2)
|
||||
self.assertEqual(len(move_byproduct_2), 1)
|
||||
self.assertEqual(move_byproduct_2.product_uom_qty, 4.0)
|
||||
self.assertEqual(move_byproduct_2.quantity_done, 0)
|
||||
self.assertEqual(len(move_byproduct_2.move_line_ids), 0)
|
||||
self.assertEqual(len(move_byproduct_2.move_line_ids), 1)
|
||||
|
||||
move_byproduct_3 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3)
|
||||
self.assertEqual(move_byproduct_3.product_uom_qty, 4.0)
|
||||
self.assertEqual(move_byproduct_3.quantity_done, 0)
|
||||
self.assertEqual(move_byproduct_3.product_uom, dozen)
|
||||
self.assertEqual(len(move_byproduct_3.move_line_ids), 0)
|
||||
self.assertEqual(len(move_byproduct_3.move_line_ids), 1)
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
@@ -1043,7 +1043,7 @@ class TestMrpOrder(TestMrpCommon):
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(move_byproduct_2, view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = self.lot_1
|
||||
ml.qty_done = 2
|
||||
details_operation_form.save()
|
||||
|
||||
@@ -485,7 +485,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
||||
"""
|
||||
|
||||
self.warehouse.mto_pull_id.route_id.active = True
|
||||
# Creating complex product which trigger another manifacture
|
||||
# Creating complex product which trigger another manufacture
|
||||
|
||||
routes = self.warehouse.manufacture_pull_id.route_id + self.warehouse.mto_pull_id.route_id
|
||||
self.complex_product = self.env['product.product'].create({
|
||||
@@ -525,6 +525,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
|
||||
production_form.picking_type_id = self.warehouse.manu_type_id
|
||||
production = production_form.save()
|
||||
production.action_confirm()
|
||||
self.env.invalidate_all()
|
||||
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 2)
|
||||
|
||||
@@ -45,7 +45,8 @@ class ChangeProductionQty(models.TransientModel):
|
||||
move.write({'product_uom_qty': move.product_uom_qty + qty})
|
||||
|
||||
if push_moves:
|
||||
push_moves._action_confirm()._action_assign()
|
||||
push_moves._action_confirm()
|
||||
production.move_finished_ids._action_assign()
|
||||
|
||||
return modification
|
||||
|
||||
|
||||
@@ -137,11 +137,11 @@ class MrpProduction(models.Model):
|
||||
'qty_done': new_quantity_done,
|
||||
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
|
||||
}
|
||||
ml.copy(default=default)
|
||||
ml.with_context(bypass_reservation_update=True).write({
|
||||
ml.write({
|
||||
'reserved_uom_qty': new_qty_reserved,
|
||||
'qty_done': 0
|
||||
})
|
||||
ml.copy(default=default)
|
||||
|
||||
if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) > 0:
|
||||
self.env['stock.move.line'].create({
|
||||
|
||||
@@ -70,6 +70,7 @@ class StockPicking(models.Model):
|
||||
amounts = [move_line.qty_done for move_line in move.move_line_ids]
|
||||
len_amounts = len(amounts)
|
||||
productions = production._split_productions({production: amounts}, set_consumed_qty=True)
|
||||
productions.move_finished_ids.move_line_ids.write({'qty_done': 0})
|
||||
for production, move_line in zip(productions, move.move_line_ids):
|
||||
if move_line.lot_id:
|
||||
production.lot_producing_id = move_line.lot_id
|
||||
|
||||
@@ -1262,10 +1262,9 @@ class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon):
|
||||
so = so_form.save()
|
||||
so.action_confirm()
|
||||
|
||||
pick_picking, pack_picking, _ = so.picking_ids
|
||||
_, pack_picking, pick_picking = so.picking_ids
|
||||
(pick_picking + pack_picking).move_ids.quantity_done = 5
|
||||
(pick_picking + pack_picking).button_validate()
|
||||
|
||||
with Form(so) as so_form:
|
||||
with so_form.order_line.edit(0) as line:
|
||||
line.product_uom_qty = 3
|
||||
|
||||
@@ -1445,6 +1445,7 @@ Please change the quantity done or the rounding precision of your unit of measur
|
||||
'company_id': self.company_id.id,
|
||||
}
|
||||
if quantity:
|
||||
# TODO could be also move in create/write
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
|
||||
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
|
||||
@@ -1502,18 +1503,13 @@ Please change the quantity done or the rounding precision of your unit of measur
|
||||
if float_compare(taken_quantity, int(taken_quantity), precision_digits=rounding) != 0:
|
||||
taken_quantity = 0
|
||||
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
if not float_is_zero(taken_quantity, precision_rounding=self.product_id.uom_id.rounding):
|
||||
quants = self.env['stock.quant']._update_reserved_quantity(
|
||||
self.product_id, location_id, taken_quantity, lot_id=lot_id,
|
||||
package_id=package_id, owner_id=owner_id, strict=strict
|
||||
)
|
||||
except UserError:
|
||||
taken_quantity = 0
|
||||
if not float_is_zero(taken_quantity, precision_rounding=self.product_id.uom_id.rounding):
|
||||
quants = self.env['stock.quant']._get_reserve_quantity(
|
||||
self.product_id, location_id, taken_quantity, lot_id=lot_id,
|
||||
package_id=package_id, owner_id=owner_id, strict=strict
|
||||
)
|
||||
|
||||
# Find a candidate move line to update or create a new one.
|
||||
serial_move_line_vals = []
|
||||
for reserved_quant, quantity in quants:
|
||||
to_update = next((line for line in self.move_line_ids if line._reservation_is_updatable(quantity, reserved_quant)), False)
|
||||
if to_update:
|
||||
@@ -1521,14 +1517,12 @@ Please change the quantity done or the rounding precision of your unit of measur
|
||||
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
|
||||
uom_quantity_back_to_product_uom = to_update.product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if to_update and float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
|
||||
to_update.with_context(bypass_reservation_update=True).reserved_uom_qty += uom_quantity
|
||||
to_update.with_context(reserved_quant=reserved_quant).reserved_uom_qty += uom_quantity
|
||||
else:
|
||||
if self.product_id.tracking == 'serial':
|
||||
# Move lines with serial tracked product_id cannot be to-update candidates. Delay the creation to speed up candidates search + create.
|
||||
serial_move_line_vals.extend([self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant) for i in range(int(quantity))])
|
||||
self.env['stock.move.line'].with_context(reserved_quant=reserved_quant).create([self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant) for i in range(int(quantity))])
|
||||
else:
|
||||
self.env['stock.move.line'].create(self._prepare_move_line_vals(quantity=quantity, reserved_quant=reserved_quant))
|
||||
self.env['stock.move.line'].create(serial_move_line_vals)
|
||||
self.env['stock.move.line'].with_context(reserved_quant=reserved_quant).create(self._prepare_move_line_vals(quantity=quantity, reserved_quant=reserved_quant))
|
||||
return taken_quantity
|
||||
|
||||
def _should_bypass_reservation(self, forced_location=False):
|
||||
|
||||
@@ -311,6 +311,26 @@ class StockMoveLine(models.Model):
|
||||
else:
|
||||
create_move(move_line)
|
||||
|
||||
for move_line in mls:
|
||||
reserved_uom_qty = move_line.reserved_uom_qty
|
||||
location = move_line.location_id
|
||||
product = move_line.product_id
|
||||
move = move_line.move_id
|
||||
if move:
|
||||
bypass_reservation = not move._should_bypass_reservation()
|
||||
else:
|
||||
bypass_reservation = product.type == 'product' and not location.should_bypass_reservation()
|
||||
if reserved_uom_qty and not self.env.context.get('bypass_reservation_update') and bypass_reservation:
|
||||
ml_uom = move_line.product_uom_id
|
||||
|
||||
reserved_qty = ml_uom._compute_quantity(reserved_uom_qty, product.uom_id, rounding_method='HALF-UP')
|
||||
reserved_quants = self.env.context.get('reserved_quant', self.env['stock.quant'])._update_reserved_quantity(
|
||||
product, location, reserved_qty, lot_id=move_line.lot_id, package_id=move_line.package_id, owner_id=move_line.owner_id, strict=True)
|
||||
|
||||
if not reserved_quants or float_compare(reserved_qty, sum(map(lambda q: q[1], reserved_quants)), product.uom_id.rounding) != 0:
|
||||
reserved_uom_qty = product.uom_id._compute_quantity(reserved_qty, ml_uom, rounding_method='HALF-UP')
|
||||
move_line.with_context(bypass_reservation=True).reserved_uom_qty = reserved_uom_qty
|
||||
|
||||
for ml, vals in zip(mls, vals_list):
|
||||
if ml.move_id and \
|
||||
ml.move_id.picking_id and \
|
||||
@@ -340,9 +360,10 @@ class StockMoveLine(models.Model):
|
||||
return mls
|
||||
|
||||
def write(self, vals):
|
||||
# Very dangerous key. It could create some desynchronization between `stock.move.line` and `stock.quant`.
|
||||
# It exists for performances purposes. Only use it if you handle `stock.quant` reservation manualy.
|
||||
if self.env.context.get('bypass_reservation_update'):
|
||||
return super(StockMoveLine, self).write(vals)
|
||||
|
||||
return super().write(vals)
|
||||
if 'product_id' in vals and any(vals.get('state', ml.state) != 'draft' and vals['product_id'] != ml.product_id.id for ml in self):
|
||||
raise UserError(_("Changing the product is only allowed in 'Draft' state."))
|
||||
|
||||
@@ -382,40 +403,35 @@ class StockMoveLine(models.Model):
|
||||
# the quants). If the new charateristics are not available on the quants, we chose to
|
||||
# reserve the maximum possible.
|
||||
if updates or 'reserved_uom_qty' in vals:
|
||||
for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned', 'confirmed'] and ml.product_id.type == 'product'):
|
||||
|
||||
for ml in self:
|
||||
if ml.product_id.type != 'product':
|
||||
continue
|
||||
if 'reserved_uom_qty' in vals:
|
||||
new_reserved_uom_qty = ml.product_uom_id._compute_quantity(
|
||||
new_reserved_qty = ml.product_uom_id._compute_quantity(
|
||||
vals['reserved_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP')
|
||||
# Make sure `reserved_uom_qty` is not negative.
|
||||
if float_compare(new_reserved_uom_qty, 0, precision_rounding=ml.product_id.uom_id.rounding) < 0:
|
||||
if float_compare(new_reserved_qty, 0, precision_rounding=ml.product_id.uom_id.rounding) < 0:
|
||||
raise UserError(_('Reserving a negative quantity is not allowed.'))
|
||||
else:
|
||||
new_reserved_uom_qty = ml.reserved_qty
|
||||
new_reserved_qty = ml.reserved_qty
|
||||
|
||||
# Unreserve the old charateristics of the move line.
|
||||
if not ml.move_id._should_bypass_reservation(ml.location_id):
|
||||
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.reserved_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||||
if not float_is_zero(ml.reserved_qty, precision_rounding=ml.product_uom_id.rounding):
|
||||
if not ml.move_id._should_bypass_reservation(ml.location_id):
|
||||
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.reserved_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||||
|
||||
# Reserve the maximum available of the new charateristics of the move line.
|
||||
reserved_qty = new_reserved_qty
|
||||
if not ml.move_id._should_bypass_reservation(updates.get('location_id', ml.location_id)):
|
||||
reserved_qty = 0
|
||||
try:
|
||||
available_qty = Quant._get_available_quantity(ml.product_id, updates.get('location_id', ml.location_id), lot_id=updates.get('lot_id', ml.lot_id),
|
||||
package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
|
||||
to_reserve = min(available_qty, new_reserved_uom_qty)
|
||||
q = []
|
||||
if to_reserve:
|
||||
q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), to_reserve, lot_id=updates.get('lot_id', ml.lot_id),
|
||||
package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
|
||||
reserved_qty = sum([x[1] for x in q])
|
||||
except UserError:
|
||||
pass
|
||||
if reserved_qty != new_reserved_uom_qty:
|
||||
new_reserved_uom_qty = ml.product_id.uom_id._compute_quantity(reserved_qty, ml.product_uom_id, rounding_method='HALF-UP')
|
||||
ml.with_context(bypass_reservation_update=True).reserved_uom_qty = new_reserved_uom_qty
|
||||
# we don't want to override the new reserved quantity
|
||||
vals.pop('reserved_uom_qty', None)
|
||||
q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), new_reserved_qty, lot_id=updates.get('lot_id', ml.lot_id),
|
||||
package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
|
||||
reserved_qty = sum([x[1] for x in q])
|
||||
|
||||
if reserved_qty != new_reserved_qty:
|
||||
reserved_uom_qty = ml.product_id.uom_id._compute_quantity(reserved_qty, ml.product_uom_id, rounding_method='HALF-UP')
|
||||
vals['reserved_uom_qty'] = reserved_uom_qty
|
||||
|
||||
if 'reserved_uom_qty' in vals and vals['reserved_uom_qty'] != ml.reserved_uom_qty:
|
||||
moves_to_recompute_state |= ml.move_id
|
||||
|
||||
# When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
|
||||
@@ -593,9 +609,6 @@ class StockMoveLine(models.Model):
|
||||
qty_done_product_uom = ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id, rounding_method='HALF-UP')
|
||||
extra_qty = qty_done_product_uom - ml.reserved_qty
|
||||
ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, ml_ids_to_ignore=ml_ids_to_ignore)
|
||||
# unreserve what's been reserved
|
||||
if not ml.move_id._should_bypass_reservation(ml.location_id) and ml.product_id.type == 'product' and ml.reserved_qty:
|
||||
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.reserved_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||||
|
||||
# move what's been actually done
|
||||
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
|
||||
@@ -610,7 +623,7 @@ class StockMoveLine(models.Model):
|
||||
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
|
||||
ml_ids_to_ignore.add(ml.id)
|
||||
# Reset the reserved quantity as we just moved it to the destination location.
|
||||
mls_todo.with_context(bypass_reservation_update=True).write({
|
||||
mls_todo.write({
|
||||
'reserved_uom_qty': 0.00,
|
||||
'date': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import logging
|
||||
|
||||
from ast import literal_eval
|
||||
from collections import defaultdict
|
||||
from psycopg2 import Error
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
@@ -625,6 +626,62 @@ class StockQuant(models.Model):
|
||||
else:
|
||||
return sum([available_quantity for available_quantity in availaible_quantities.values() if float_compare(available_quantity, 0, precision_rounding=rounding) > 0])
|
||||
|
||||
def _get_reserve_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, strict=False):
|
||||
""" Get the quantity available to reserve for the set of quants
|
||||
sharing the combination of `product_id, location_id` if `strict` is set to False or sharing
|
||||
the *exact same characteristics* otherwise. Typically, this method is called before the
|
||||
`stock.move.line` creation to know the reserved_qty that could be use. It's also call by
|
||||
`_update_reserve_quantity` to find the quant to reserve.
|
||||
|
||||
:return: a list of tuples (quant, quantity_reserved) showing on which quant the reservation
|
||||
could be done and how much the system is able to reserve on it
|
||||
"""
|
||||
self = self.sudo()
|
||||
rounding = product_id.uom_id.rounding
|
||||
quants = self or self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
reserved_quants = []
|
||||
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0:
|
||||
# if we want to reserve
|
||||
available_quantity = sum(quants.filtered(lambda q: float_compare(q.quantity, 0, precision_rounding=rounding) > 0).mapped('quantity')) - sum(quants.mapped('reserved_quantity'))
|
||||
elif float_compare(quantity, 0, precision_rounding=rounding) < 0:
|
||||
# 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))
|
||||
else:
|
||||
return reserved_quants
|
||||
|
||||
negative_reserved_quantity = defaultdict(float)
|
||||
for quant in quants:
|
||||
if float_compare(quant.quantity - quant.reserved_quantity, 0, precision_rounding=rounding) < 0:
|
||||
negative_reserved_quantity[(quant.location_id, quant.lot_id, quant.package_id, quant.owner_id)] += quant.quantity - quant.reserved_quantity
|
||||
for quant in quants:
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0:
|
||||
max_quantity_on_quant = quant.quantity - quant.reserved_quantity
|
||||
if float_compare(max_quantity_on_quant, 0, precision_rounding=rounding) <= 0:
|
||||
continue
|
||||
negative_quantity = negative_reserved_quantity[(quant.location_id, quant.lot_id, quant.package_id, quant.owner_id)]
|
||||
if negative_quantity:
|
||||
negative_qty_to_remove = min(abs(negative_quantity), max_quantity_on_quant)
|
||||
negative_reserved_quantity[(quant.location_id, quant.lot_id, quant.package_id, quant.owner_id)] += negative_qty_to_remove
|
||||
max_quantity_on_quant -= negative_qty_to_remove
|
||||
if float_compare(max_quantity_on_quant, 0, precision_rounding=rounding) <= 0:
|
||||
continue
|
||||
max_quantity_on_quant = min(max_quantity_on_quant, quantity)
|
||||
reserved_quants.append((quant, max_quantity_on_quant))
|
||||
quantity -= max_quantity_on_quant
|
||||
available_quantity -= max_quantity_on_quant
|
||||
else:
|
||||
max_quantity_on_quant = min(quant.reserved_quantity, abs(quantity))
|
||||
reserved_quants.append((quant, -max_quantity_on_quant))
|
||||
quantity += max_quantity_on_quant
|
||||
available_quantity += max_quantity_on_quant
|
||||
|
||||
if float_is_zero(quantity, precision_rounding=rounding) or float_is_zero(available_quantity, precision_rounding=rounding):
|
||||
break
|
||||
return reserved_quants
|
||||
|
||||
@api.onchange('location_id', 'product_id', 'lot_id', 'package_id', 'owner_id')
|
||||
def _onchange_location_or_product_id(self):
|
||||
vals = {}
|
||||
@@ -773,7 +830,6 @@ class StockQuant(models.Model):
|
||||
})
|
||||
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
|
||||
|
||||
@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
|
||||
sharing the combination of `product_id, location_id` if `strict` is set to False or sharing
|
||||
@@ -787,43 +843,10 @@ class StockQuant(models.Model):
|
||||
was done and how much the system was able to reserve on it
|
||||
"""
|
||||
self = self.sudo()
|
||||
rounding = product_id.uom_id.rounding
|
||||
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
reserved_quants = []
|
||||
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0:
|
||||
# if we want to reserve
|
||||
available_quantity = sum(quants.filtered(lambda q: float_compare(q.quantity, 0, precision_rounding=rounding) > 0).mapped('quantity')) - sum(quants.mapped('reserved_quantity'))
|
||||
if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to reserve more products of %s than you have in stock.', product_id.display_name))
|
||||
elif float_compare(quantity, 0, precision_rounding=rounding) < 0:
|
||||
# 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))
|
||||
else:
|
||||
return reserved_quants
|
||||
|
||||
for quant in quants:
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0:
|
||||
max_quantity_on_quant = quant.quantity - quant.reserved_quantity
|
||||
if float_compare(max_quantity_on_quant, 0, precision_rounding=rounding) <= 0:
|
||||
continue
|
||||
max_quantity_on_quant = min(max_quantity_on_quant, quantity)
|
||||
quant.reserved_quantity += max_quantity_on_quant
|
||||
reserved_quants.append((quant, max_quantity_on_quant))
|
||||
quantity -= max_quantity_on_quant
|
||||
available_quantity -= max_quantity_on_quant
|
||||
else:
|
||||
max_quantity_on_quant = min(quant.reserved_quantity, abs(quantity))
|
||||
quant.reserved_quantity -= max_quantity_on_quant
|
||||
reserved_quants.append((quant, -max_quantity_on_quant))
|
||||
quantity += max_quantity_on_quant
|
||||
available_quantity += max_quantity_on_quant
|
||||
|
||||
if float_is_zero(quantity, precision_rounding=rounding) or float_is_zero(available_quantity, precision_rounding=rounding):
|
||||
break
|
||||
return reserved_quants
|
||||
quants_to_reserve = self._get_reserve_quantity(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
for quant, qty_to_reserve in quants_to_reserve:
|
||||
quant.reserved_quantity += qty_to_reserve
|
||||
return quants_to_reserve
|
||||
|
||||
@api.model
|
||||
def _unlink_zero_quants(self):
|
||||
|
||||
@@ -36,6 +36,7 @@ class StockGenerate(TransactionCase):
|
||||
|
||||
def get_new_move(self, nbre_of_lines):
|
||||
move_lines_val = []
|
||||
self.env['stock.quant']._update_available_quantity(self.product_serial, self.location, nbre_of_lines)
|
||||
for i in range(nbre_of_lines):
|
||||
move_lines_val.append({
|
||||
'product_id': self.product_serial.id,
|
||||
|
||||
@@ -393,6 +393,7 @@ class TestInventory(TransactionCase):
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 4.0,
|
||||
})
|
||||
quant.invalidate_recordset()
|
||||
move_out._action_confirm()
|
||||
move_out._action_assign()
|
||||
move_out.move_line_ids.qty_done = 4
|
||||
|
||||
@@ -3612,8 +3612,9 @@ class StockMove(TransactionCase):
|
||||
def test_edit_reserved_move_line_9(self):
|
||||
"""
|
||||
When writing on the reserved quantity on the SML, a process tries to
|
||||
reserve the quants with that new quantity. If the written quantity is
|
||||
more than actually available, this quantity should be set to the available quantity.
|
||||
reserve the quants with that new quantity. If it fails (for instance
|
||||
because the written quantity is more than actually available), it should
|
||||
take the maximum available.
|
||||
"""
|
||||
self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
|
||||
|
||||
@@ -3632,7 +3633,7 @@ class StockMove(TransactionCase):
|
||||
out_move.move_line_ids.reserved_uom_qty = 2
|
||||
|
||||
self.assertTrue(out_move.move_line_ids)
|
||||
self.assertEqual(out_move.move_line_ids.reserved_uom_qty, 1, "The reserved quantity should be what is available")
|
||||
self.assertEqual(out_move.move_line_ids.reserved_uom_qty, 1, "The maximum available still one")
|
||||
|
||||
def test_edit_done_move_line_1(self):
|
||||
""" Test that editing a done stock move line linked to an untracked product correctly and
|
||||
|
||||
@@ -410,15 +410,15 @@ class StockQuant(TransactionCase):
|
||||
})
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
|
||||
self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2)
|
||||
with self.assertRaises(UserError):
|
||||
self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0)
|
||||
reserved_quants = self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0)
|
||||
self.assertFalse(reserved_quants)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
|
||||
|
||||
def test_increase_reserved_quantity_5(self):
|
||||
""" Decrease the available quantity when no quant are in a location.
|
||||
"""
|
||||
with self.assertRaises(UserError):
|
||||
self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 1.0)
|
||||
reserved_quants = self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 1.0)
|
||||
self.assertFalse(reserved_quants)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
|
||||
|
||||
def test_decrease_reserved_quantity_1(self):
|
||||
@@ -437,8 +437,7 @@ class StockQuant(TransactionCase):
|
||||
def test_increase_decrease_reserved_quantity_1(self):
|
||||
""" Decrease then increase reserved quantity when no quant are in a location.
|
||||
"""
|
||||
with self.assertRaises(UserError):
|
||||
self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 1.0)
|
||||
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):
|
||||
self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, -1.0, strict=True)
|
||||
|
||||
@@ -1319,7 +1319,7 @@ class TestStockFlow(TestStockCommon):
|
||||
pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'})
|
||||
picking_in.move_line_ids[0].result_package_id = pack1
|
||||
picking_in.move_line_ids[0].qty_done = 4
|
||||
packop2 = picking_in.move_line_ids[0].with_context(bypass_reservation_update=True).copy({'reserved_uom_qty': 0})
|
||||
packop2 = picking_in.move_line_ids[0].copy({'reserved_uom_qty': 0})
|
||||
packop2.qty_done = 6
|
||||
packop2.result_package_id = pack2
|
||||
picking_in._action_done()
|
||||
@@ -1343,7 +1343,7 @@ class TestStockFlow(TestStockCommon):
|
||||
picking_out.action_confirm()
|
||||
picking_out.action_assign()
|
||||
packout1 = picking_out.move_line_ids[0]
|
||||
packout2 = picking_out.move_line_ids[0].with_context(bypass_reservation_update=True).copy({'reserved_uom_qty': 0})
|
||||
packout2 = picking_out.move_line_ids[0].copy({'reserved_uom_qty': 0})
|
||||
packout1.qty_done = 2
|
||||
packout1.package_id = pack1
|
||||
packout2.package_id = pack2
|
||||
@@ -1377,7 +1377,7 @@ class TestStockFlow(TestStockCommon):
|
||||
pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'})
|
||||
picking_in.move_line_ids[0].result_package_id = pack1
|
||||
picking_in.move_line_ids[0].qty_done = 120
|
||||
packop2 = picking_in.move_line_ids[0].with_context(bypass_reservation_update=True).copy({'reserved_uom_qty': 0})
|
||||
packop2 = picking_in.move_line_ids[0].copy({'reserved_uom_qty': 0})
|
||||
packop2.qty_done = 80
|
||||
packop2.result_package_id = pack2
|
||||
picking_in._action_done()
|
||||
|
||||
Reference in New Issue
Block a user