feat: implement transaction-level session locking in pos_order and idempotent payment creation in pos_payment

This commit is contained in:
Suherdy Yacob 2026-06-19 10:57:51 +07:00
parent 52565d3f92
commit 932db6e5f7
3 changed files with 81 additions and 0 deletions

View File

@ -2,4 +2,5 @@
from . import pos_config from . import pos_config
from . import res_config_settings from . import res_config_settings
from . import pos_order from . import pos_order
from . import pos_payment

View File

@ -106,7 +106,34 @@ class PosOrder(models.Model):
Override to prevent concurrent processing of the same order uuid. Override to prevent concurrent processing of the same order uuid.
Uses pg_advisory_xact_lock on the order's uuid to force subsequent Uses pg_advisory_xact_lock on the order's uuid to force subsequent
duplicate requests to wait until the first request completes/commits. duplicate requests to wait until the first request completes/commits.
Also, locks the session associated with the incoming orders at the database
transaction level to serialize updates affecting the same session and prevent
serialization failures (concurrent updates on pos.session).
""" """
# Extract unique session IDs from the orders list
session_ids = set()
for order in orders:
session_id = order.get('session_id')
if session_id:
session_ids.add(session_id)
if session_ids:
# Also find if any of these sessions are closed/closing, and if so, their potential open rescue/config sessions
sessions = self.env['pos.session'].browse(session_ids).exists()
closed_or_closing = sessions.filtered(lambda s: s.state in ('closing_control', 'closed'))
if closed_or_closing:
# Find open sessions for those configs
open_sessions = self.env['pos.session'].search([
('state', '=', 'opened'),
('config_id', 'in', closed_or_closing.mapped('config_id').ids)
])
session_ids.update(open_sessions.ids)
# De-duplicate, filter to existing, sort to prevent deadlocks, and lock using FOR UPDATE
resolved_session_ids = sorted(list(session_ids))
if resolved_session_ids:
self.env.cr.execute("SELECT id FROM pos_session WHERE id IN %s FOR UPDATE", (tuple(resolved_session_ids),))
# Sort orders by uuid to prevent potential deadlocks when locking multiple orders # Sort orders by uuid to prevent potential deadlocks when locking multiple orders
sorted_orders = sorted(orders, key=lambda x: x.get('uuid') or '') sorted_orders = sorted(orders, key=lambda x: x.get('uuid') or '')
for order in sorted_orders: for order in sorted_orders:

53
models/pos_payment.py Normal file
View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from odoo import models, api
class PosPayment(models.Model):
_inherit = 'pos.payment'
@api.model_create_multi
def create(self, vals_list):
# 1. Extract and sort unique uuids to prevent deadlocks
uuids = []
for vals in vals_list:
if isinstance(vals, dict) and vals.get('uuid'):
uuids.append(vals['uuid'])
sorted_uuids = sorted(list(set(uuids)))
# 2. Acquire transaction-level PostgreSQL advisory locks on the sorted hashes of uuids
for uuid in sorted_uuids:
self.env.cr.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (uuid,))
# 3. Check which payments already exist in the database
existing_payments_map = {}
if sorted_uuids:
existing_payments = self.search([('uuid', 'in', sorted_uuids)])
for payment in existing_payments:
existing_payments_map[payment.uuid] = payment
# 4. Prepare values to create (only those that don't already exist)
final_payments_list = [None] * len(vals_list)
vals_to_create = []
create_indices = []
for i, vals in enumerate(vals_list):
uuid = vals.get('uuid') if isinstance(vals, dict) else None
if uuid and uuid in existing_payments_map:
final_payments_list[i] = existing_payments_map[uuid]
else:
vals_to_create.append(vals)
create_indices.append(i)
# 5. Create new payments
if vals_to_create:
created_payments = super(PosPayment, self).create(vals_to_create)
for idx, payment in zip(create_indices, created_payments):
final_payments_list[idx] = payment
# 6. Reconstruct and return the final combined recordset
final_payments = self.env['pos.payment']
for payment in final_payments_list:
if payment:
final_payments += payment
return final_payments