[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:
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user