[FIX] point_of_sale: speed-up picking creation at closing
Forward-port of PR odoo/odoo#86643. Add a unittest for multi_step route on Ship Later warehouse to ensure that the fix 5959d77f344 is not unintentionally reverted. closes odoo/odoo#89595 X-original-commit: d5fb77e9d4716dd509e2e98ff39f7e1cb91ddaca Signed-off-by: Masereel Pierre <pim@odoo.com>
This commit is contained in:
committed by
Pierre Masereel
parent
84b847020f
commit
1412c6edf2
@@ -6,6 +6,7 @@ from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_is_zero, float_compare
|
||||
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit='stock.picking'
|
||||
@@ -86,13 +87,13 @@ class StockPicking(models.Model):
|
||||
def _create_move_from_pos_order_lines(self, lines):
|
||||
self.ensure_one()
|
||||
lines_by_product = groupby(sorted(lines, key=lambda l: l.product_id.id), key=lambda l: l.product_id.id)
|
||||
for product, lines in lines_by_product:
|
||||
order_lines = self.env['pos.order.line'].concat(*lines)
|
||||
current_move = self.env['stock.move'].create(
|
||||
self._prepare_stock_move_vals(order_lines[0], order_lines)
|
||||
)
|
||||
confirmed_moves = current_move._action_confirm()
|
||||
confirmed_moves._add_mls_related_to_order(order_lines)
|
||||
move_vals = []
|
||||
for dummy, olines in lines_by_product:
|
||||
order_lines = self.env['pos.order.line'].concat(*olines)
|
||||
move_vals.append(self._prepare_stock_move_vals(order_lines[0], order_lines))
|
||||
moves = self.env['stock.move'].create(move_vals)
|
||||
confirmed_moves = moves._action_confirm()
|
||||
confirmed_moves._add_mls_related_to_order(lines, are_qties_done=True)
|
||||
|
||||
def _send_confirmation_email(self):
|
||||
# Avoid sending Mail/SMS for POS deliveries
|
||||
@@ -129,71 +130,111 @@ class StockMove(models.Model):
|
||||
keys = super(StockMove, self)._key_assign_picking()
|
||||
return keys + (self.group_id.pos_order_id,)
|
||||
|
||||
def _complete_done_qties(self):
|
||||
self.ensure_one()
|
||||
# from commit
|
||||
@api.model
|
||||
def _prepare_lines_data_dict(self, order_lines):
|
||||
lines_data = defaultdict(dict)
|
||||
for product_id, olines in groupby(sorted(order_lines, key=lambda l: l.product_id.id), key=lambda l: l.product_id.id):
|
||||
lines_data[product_id].update({'order_lines': self.env['pos.order.line'].concat(*olines)})
|
||||
return lines_data
|
||||
|
||||
def _complete_done_qties(self, set_quantity_done_on_move=False):
|
||||
self._action_assign()
|
||||
for move_line in self.move_line_ids:
|
||||
move_line.qty_done = move_line.reserved_uom_qty
|
||||
if float_compare(self.product_uom_qty, self.quantity_done, precision_rounding=self.product_uom.rounding) > 0:
|
||||
remaining_qty = self.product_uom_qty - self.quantity_done
|
||||
ml_vals = self._prepare_move_line_vals()
|
||||
ml_vals.update({'qty_done': remaining_qty})
|
||||
self.env['stock.move.line'].create(ml_vals)
|
||||
mls_vals = []
|
||||
moves_to_set = set()
|
||||
for move in self:
|
||||
if float_compare(move.product_uom_qty, move.quantity_done, precision_rounding=move.product_uom.rounding) > 0:
|
||||
remaining_qty = move.product_uom_qty - move.quantity_done
|
||||
mls_vals.append(dict(move._prepare_move_line_vals(), qty_done=remaining_qty))
|
||||
moves_to_set.add(move.id)
|
||||
self.env['stock.move.line'].create(mls_vals)
|
||||
if set_quantity_done_on_move:
|
||||
for move in self.env['stock.move'].browse(moves_to_set):
|
||||
move.quantity_done = move.product_uom_qty
|
||||
|
||||
def _create_production_lots_for_pos_order(self, lines):
|
||||
''' Search for existing lots and create missing ones.
|
||||
|
||||
:param lines: pos order lines with pack lot ids.
|
||||
:type lines: pos.order.line recordset.
|
||||
|
||||
:return stock.lot recordset.
|
||||
'''
|
||||
valid_lots = self.env['stock.lot']
|
||||
moves = self.filtered(lambda m: m.picking_type_id.use_existing_lots)
|
||||
# Already called in self._action_confirm() but just to be safe when coming from _launch_stock_rule_from_pos_order_lines.
|
||||
self._check_company()
|
||||
if moves:
|
||||
moves_product_ids = set(moves.mapped('product_id').ids)
|
||||
lots = lines.pack_lot_ids.filtered(lambda l: l.lot_name and l.product_id.id in moves_product_ids)
|
||||
lots_data = set(lots.mapped(lambda l: (l.product_id.id, l.lot_name)))
|
||||
existing_lots = self.env['stock.lot'].search([
|
||||
('company_id', '=', moves[0].picking_type_id.company_id.id),
|
||||
('product_id', 'in', lines.product_id.ids),
|
||||
('name', 'in', lots.mapped('lot_name')),
|
||||
])
|
||||
#The previous search may return (product_id.id, lot_name) combinations that have no matching in lines.pack_lot_ids.
|
||||
for lot in existing_lots:
|
||||
if (lot.product_id.id, lot.name) in lots_data:
|
||||
valid_lots |= lot
|
||||
lots_data.remove((lot.product_id.id, lot.name))
|
||||
moves = moves.filtered(lambda m: m.picking_type_id.use_create_lots)
|
||||
if moves:
|
||||
moves_product_ids = set(moves.mapped('product_id').ids)
|
||||
missing_lot_values = []
|
||||
for lot_product_id, lot_name in filter(lambda l: l[0] in moves_product_ids, lots_data):
|
||||
missing_lot_values.append({'company_id': self.company_id.id, 'product_id': lot_product_id, 'name': lot_name})
|
||||
valid_lots |= self.env['stock.lot'].create(missing_lot_values)
|
||||
return valid_lots
|
||||
|
||||
def _add_mls_related_to_order(self, related_order_lines, are_qties_done=True):
|
||||
lines_data = self._prepare_lines_data_dict(related_order_lines)
|
||||
qty_fname = 'qty_done' if are_qties_done else 'reserved_uom_qty'
|
||||
for move in self:
|
||||
if related_order_lines[0].product_id == move.product_id and related_order_lines[0].product_id.tracking != 'none':
|
||||
if move.picking_type_id.use_existing_lots or move.picking_type_id.use_create_lots:
|
||||
for line in related_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 = move._prepare_move_line_vals()
|
||||
if move.picking_type_id.use_existing_lots:
|
||||
existing_lot = self.env['stock.lot'].search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('product_id', '=', line.product_id.id),
|
||||
('name', '=', lot.lot_name)
|
||||
])
|
||||
if not existing_lot and move.picking_type_id.use_create_lots:
|
||||
existing_lot = self.env['stock.lot'].create({
|
||||
'company_id': self.company_id.id,
|
||||
'product_id': line.product_id.id,
|
||||
'name': lot.lot_name,
|
||||
})
|
||||
quant = existing_lot.quant_ids.filtered(lambda q: q.quantity > 0.0 and q.location_id.parent_path.startswith(move.location_id.parent_path))[-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,
|
||||
})
|
||||
ml = self.env['stock.move.line'].create(ml_vals)
|
||||
ml.write({qty_fname: qty})
|
||||
sum_of_lots += qty
|
||||
if abs(line.qty) != sum_of_lots:
|
||||
difference_qty = abs(line.qty) - sum_of_lots
|
||||
ml_vals = self[0]._prepare_move_line_vals()
|
||||
if line.product_id.tracking == 'serial':
|
||||
mls = self.env['stock.move.line']
|
||||
for dummy in range(int(difference_qty)):
|
||||
mls |= self.env['stock.move.line'].create(ml_vals)
|
||||
mls.write({qty_fname: 1})
|
||||
else:
|
||||
ml = self.env['stock.move.line'].create(ml_vals)
|
||||
ml.write({qty_fname: difference_qty})
|
||||
else:
|
||||
move._action_assign()
|
||||
if are_qties_done:
|
||||
move._complete_done_qties()
|
||||
else:
|
||||
move._action_assign()
|
||||
if are_qties_done:
|
||||
move._complete_done_qties()
|
||||
move.quantity_done = move.product_uom_qty
|
||||
# Moves with product_id not in related_order_lines. This can happend e.g. when product_id has a phantom-type bom.
|
||||
moves_to_assign = self.filtered(lambda m: m.product_id.id not in lines_data or m.product_id.tracking == 'none'
|
||||
or (not m.picking_type_id.use_existing_lots and not m.picking_type_id.use_create_lots))
|
||||
moves_to_assign._complete_done_qties(set_quantity_done_on_move=True)
|
||||
moves_remaining = self - moves_to_assign
|
||||
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 = self[0]._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})
|
||||
|
||||
@@ -473,10 +473,12 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
pos_make_payment.with_context(context_payment).check()
|
||||
|
||||
self.assertEqual(pos_order.state, 'paid')
|
||||
self.assertEqual(pos_order.picking_ids.move_line_ids[0].lot_id, lot)
|
||||
self.assertFalse(pos_order.picking_ids.move_line_ids[1].lot_id)
|
||||
self.assertEqual(pos_order.picking_ids.move_line_ids[0].location_id, shelf1_location)
|
||||
self.assertEqual(pos_order.picking_ids.move_line_ids[1].location_id, shelf1_location)
|
||||
tracked_line = pos_order.picking_ids.move_line_ids.filtered(lambda ml: ml.product_id.id == tracked_product.id)
|
||||
untracked_line = pos_order.picking_ids.move_line_ids - tracked_line
|
||||
self.assertEqual(tracked_line.lot_id, lot)
|
||||
self.assertFalse(untracked_line.lot_id)
|
||||
self.assertEqual(tracked_line.location_id, shelf1_location)
|
||||
self.assertEqual(untracked_line.location_id, shelf1_location)
|
||||
|
||||
self.pos_config.current_session_id.action_pos_session_closing_control()
|
||||
|
||||
@@ -1086,3 +1088,61 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
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
|
||||
and multiple move_lines.
|
||||
"""
|
||||
tracked_product = self.env['product.product'].create({
|
||||
'name': 'SuperProduct Tracked',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
'available_in_pos': True
|
||||
})
|
||||
warehouse_id = self.company_data['default_warehouse']
|
||||
warehouse_id.delivery_steps = 'pick_ship'
|
||||
|
||||
self.pos_config.ship_later = True
|
||||
self.pos_config.warehouse_id = warehouse_id
|
||||
self.pos_config.route_id = warehouse_id.route_ids[-1]
|
||||
self.pos_config.open_session_cb()
|
||||
self.pos_config.current_session_id.update_stock_at_closing = False
|
||||
|
||||
untax, tax = self.compute_tax(tracked_product, 1.15, 1)
|
||||
|
||||
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': 1.15,
|
||||
'qty': 1.0,
|
||||
'price_subtotal': untax,
|
||||
'price_subtotal_incl': untax + tax,
|
||||
})],
|
||||
'amount_tax': tax,
|
||||
'amount_total': untax+tax,
|
||||
'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': untax+tax,
|
||||
})
|
||||
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
|
||||
self.assertEqual(pos_order.state, 'paid')
|
||||
self.assertEqual(len(picking_mls), 2)
|
||||
self.assertEqual(len(pickings.picking_type_id), 2)
|
||||
|
||||
Reference in New Issue
Block a user