[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:
Aurélien (avd)
2022-04-25 18:15:14 +02:00
committed by Pierre Masereel
parent 84b847020f
commit 1412c6edf2
2 changed files with 175 additions and 74 deletions
+111 -70
View File
@@ -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)