[FIX] point_of_sale: avoid unreserve more than reserved

When you are shipping later from POS and have a multi-step
configuration, you get an error saying that you cannot unreserve more
than what is reserved. It comes form a bad filtered list of move.

Now as we are correctly filtering the move on their product_id it
correctly works.

OPW-2854159

closes odoo/odoo#93336

X-original-commit: 93e0509d0ba7302ecdf5d0f6dbc2dbe90e98229d
Signed-off-by: Masereel Pierre <pim@odoo.com>
This commit is contained in:
Pierre Masereel
2022-06-10 20:40:21 +02:00
parent a84f6965ee
commit e65ddaf2d7
3 changed files with 93 additions and 120 deletions
+3 -5
View File
@@ -1091,13 +1091,11 @@ class PosOrderLine(models.Model):
pickings_to_confirm.action_confirm()
tracked_lines = order.lines.filtered(lambda l: l.product_id.tracking != 'none')
lines_by_tracked_product = groupby(sorted(tracked_lines, key=lambda l: l.product_id.id), key=lambda l: l.product_id.id)
mls_to_unlink = self.env['stock.move.line']
for dummy, lines in lines_by_tracked_product:
for product_id, lines in lines_by_tracked_product:
lines = self.env['pos.order.line'].concat(*lines)
moves = pickings_to_confirm.move_ids.filtered(lambda m: m.product_id in tracked_lines.product_id)
mls_to_unlink |= moves.move_line_ids
moves = pickings_to_confirm.move_ids.filtered(lambda m: m.product_id.id == product_id)
moves.move_line_ids.unlink()
moves._add_mls_related_to_order(lines, are_qties_done=False)
mls_to_unlink.unlink()
return True
def _is_product_storable_fifo_avco(self):
+54 -38
View File
@@ -199,42 +199,58 @@ class StockMove(models.Model):
existing_lots = moves_remaining._create_production_lots_for_pos_order(related_order_lines)
move_lines_to_create = []
mls_qties = []
for move in moves_remaining:
for line in lines_data[move.product_id.id]['order_lines']:
sum_of_lots = 0
for lot in line.pack_lot_ids.filtered(lambda l: l.lot_name):
if line.product_id.tracking == 'serial':
qty = 1
else:
qty = abs(line.qty)
ml_vals = dict(move._prepare_move_line_vals())
if existing_lots:
existing_lot = existing_lots.filtered_domain([('product_id', '=', line.product_id.id), ('name', '=', lot.lot_name)])
quant = self.env['stock.quant']
if existing_lot:
quant = self.env['stock.quant'].search(
[('lot_id', '=', existing_lot.id), ('quantity', '>', '0.0'), ('location_id', 'child_of', move.location_id.id)],
order='id desc',
limit=1
)
ml_vals.update({
'lot_id': existing_lot.id,
'location_id': quant.location_id.id or move.location_id.id
})
else:
ml_vals.update({'lot_name': lot.lot_name})
move_lines_to_create.append(ml_vals)
mls_qties.append(qty)
sum_of_lots += qty
if abs(line.qty) != sum_of_lots:
difference_qty = abs(line.qty) - sum_of_lots
ml_vals = move._prepare_move_line_vals()
if line.product_id.tracking == 'serial':
move_lines_to_create.extend([ml_vals for i in range(int(difference_qty))])
mls_qties.extend([1]*int(difference_qty))
else:
if are_qties_done:
for move in moves_remaining:
for line in lines_data[move.product_id.id]['order_lines']:
sum_of_lots = 0
for lot in line.pack_lot_ids.filtered(lambda l: l.lot_name):
if line.product_id.tracking == 'serial':
qty = 1
else:
qty = abs(line.qty)
ml_vals = dict(move._prepare_move_line_vals())
if existing_lots:
existing_lot = existing_lots.filtered_domain([('product_id', '=', line.product_id.id), ('name', '=', lot.lot_name)])
quant = self.env['stock.quant']
if existing_lot:
quant = self.env['stock.quant'].search(
[('lot_id', '=', existing_lot.id), ('quantity', '>', '0.0'), ('location_id', 'child_of', move.location_id.id)],
order='id desc',
limit=1
)
ml_vals.update({
'lot_id': existing_lot.id,
'location_id': quant.location_id.id or move.location_id.id
})
else:
ml_vals.update({'lot_name': lot.lot_name})
move_lines_to_create.append(ml_vals)
mls_qties.append(difference_qty)
move_lines = self.env['stock.move.line'].create(move_lines_to_create)
for move_line, qty in zip(move_lines, mls_qties):
move_line.write({qty_fname: qty})
mls_qties.append(qty)
sum_of_lots += qty
if abs(line.qty) != sum_of_lots:
difference_qty = abs(line.qty) - sum_of_lots
ml_vals = move._prepare_move_line_vals()
if line.product_id.tracking == 'serial':
move_lines_to_create.extend([ml_vals for i in range(int(difference_qty))])
mls_qties.extend([1]*int(difference_qty))
else:
move_lines_to_create.append(ml_vals)
mls_qties.append(difference_qty)
move_lines = self.env['stock.move.line'].create(move_lines_to_create)
for move_line, qty in zip(move_lines, mls_qties):
move_line.write({qty_fname: qty})
else:
for move in moves_remaining:
for line in lines_data[move.product_id.id]['order_lines']:
for lot in line.pack_lot_ids.filtered(lambda l: l.lot_name):
if line.product_id.tracking == 'serial':
qty = 1
else:
qty = abs(line.qty)
if existing_lots:
existing_lot = existing_lots.filtered_domain([('product_id', '=', line.product_id.id), ('name', '=', lot.lot_name)])
if existing_lot:
available_quantity = move._get_available_quantity(move.location_id, lot_id=existing_lot, strict=True)
if not float_is_zero(available_quantity, precision_rounding=line.product_id.uom_id.rounding):
move._update_reserved_quantity(qty, min(qty, available_quantity), move.location_id, existing_lot)
continue
@@ -1014,80 +1014,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
diff_line = pos_session.move_id.line_ids.filtered(lambda line: line.name == 'Difference at closing PoS session')
self.assertAlmostEqual(diff_line.credit, 5.0, msg="Missing amount of 5.0")
def test_order_and_ship_later(self):
""" In case the product is tracked and the "Ship Later" feature is enabled, this test ensures
that the lots defined on the POS order are the same that the ones on the SMLs of the associated
picking"""
self.env['stock.picking.type'].search([('code', '=', 'outgoing')]).write({
'use_create_lots': True,
'use_existing_lots': True,
})
tracked_product = self.env['product.product'].create({
'name': 'SuperProduct Tracked',
'type': 'product',
'tracking': 'lot',
'available_in_pos': True,
})
lot01, lot02 = self.env['stock.lot'].create([{
'name': name,
'product_id': tracked_product.id,
'company_id': self.env.company.id,
} for name in ['Lot01', 'Lot02']])
stock_location = self.company_data['default_warehouse'].lot_stock_id
self.env['stock.quant']._update_available_quantity(tracked_product, stock_location, 10, lot_id=lot01)
self.env['stock.quant']._update_available_quantity(tracked_product, stock_location, 10, lot_id=lot02)
self.pos_config.ship_later = True
self.pos_config.open_session_cb()
# Create a POS order with:
# 1 x Lot01
# 1 x Lot02
# 1 x Lot03 (-> nonexistent lot!)
pos_order = self.PosOrder.create({
'company_id': self.env.company.id,
'session_id': self.pos_config.current_session_id.id,
'pricelist_id': self.partner1.property_product_pricelist.id,
'partner_id': self.partner1.id,
'lines': [(0, 0, {
'name': "OL/0001",
'product_id': tracked_product.id,
'price_unit': 5,
'discount': 0.0,
'qty': 1.0,
'tax_ids': [(6, 0, [])],
'price_subtotal': 5,
'price_subtotal_incl': 5,
'pack_lot_ids': [[0, 0, {'lot_name': lot_name}]],
}) for lot_name in [lot01.name, lot02.name, 'Lot03']],
'amount_tax': 10,
'amount_total': 10,
'amount_paid': 0,
'amount_return': 0,
'to_ship': True,
})
context_make_payment = {
"active_ids": [pos_order.id],
"active_id": pos_order.id,
}
pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({
'amount': 10,
})
pos_make_payment.with_context(active_id=pos_order.id).check()
picking = pos_order.picking_ids
picking_mls = picking.move_line_ids
lot01_ml = picking_mls.filtered(lambda ml: ml.lot_id == lot01)
lot02_ml = picking_mls.filtered(lambda ml: ml.lot_id == lot02)
lot03_ml = picking_mls - lot01_ml - lot02_ml
self.assertEqual(lot01_ml.reserved_qty, 1)
self.assertEqual(lot02_ml.reserved_qty, 1)
self.assertEqual(lot03_ml.reserved_qty, 1)
self.assertEqual(lot03_ml.lot_id.name, "Lot03")
def test_order_multi_step_route(self):
""" Test that orders in sessions with "Ship Later" enabled and "Specific Route" set to a
multi-step (2/3) route can be validated. This config implies multiple picking types
@@ -1099,6 +1025,24 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
'tracking': 'lot',
'available_in_pos': True
})
tracked_product_2 = self.env['product.product'].create({
'name': 'SuperProduct Tracked 2',
'type': 'product',
'tracking': 'lot',
'available_in_pos': True
})
tracked_product_2_lot = self.env['stock.lot'].create({
'name': '80085',
'product_id': tracked_product_2.id,
'company_id': self.env.company.id,
})
stock_location = self.company_data['default_warehouse'].lot_stock_id
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': tracked_product_2.id,
'inventory_quantity': 1,
'location_id': stock_location.id,
'lot_id': tracked_product_2_lot.id
}).action_apply_inventory()
warehouse_id = self.company_data['default_warehouse']
warehouse_id.delivery_steps = 'pick_ship'
@@ -1122,6 +1066,20 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
'qty': 1.0,
'price_subtotal': untax,
'price_subtotal_incl': untax + tax,
'pack_lot_ids': [
[0, 0, {'lot_name': '80085'}],
]
}),
(0, 0, {
'name': "OL/0002",
'product_id': tracked_product_2.id,
'price_unit': 1.15,
'qty': 1.0,
'price_subtotal': untax,
'price_subtotal_incl': untax + tax,
'pack_lot_ids': [
[0, 0, {'lot_name': '80085'}],
]
})],
'amount_tax': tax,
'amount_total': untax+tax,
@@ -1139,9 +1097,10 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
})
context_payment = {'active_id': pos_order.id}
pos_make_payment.with_context(context_payment).check()
pickings = pos_order.picking_ids
picking_mls = pickings.move_line_ids
picking_mls_no_stock = pickings.move_line_ids.filtered(lambda l: l.product_id.id == tracked_product.id)
picking_mls_stock = pickings.move_line_ids.filtered(lambda l: l.product_id.id == tracked_product_2.id)
self.assertEqual(pos_order.state, 'paid')
self.assertEqual(len(picking_mls), 2)
self.assertEqual(len(picking_mls_no_stock), 0)
self.assertEqual(len(picking_mls_stock), 1)
self.assertEqual(len(pickings.picking_type_id), 2)