148 lines
6.5 KiB
Python
148 lines
6.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
from odoo import fields, models, api
|
|
from odoo.fields import Domain
|
|
from datetime import datetime
|
|
import pytz
|
|
|
|
|
|
class PosOrder(models.Model):
|
|
_inherit = 'pos.order'
|
|
|
|
@api.model
|
|
def search_paid_order_ids(self, config_id, domain, limit, offset):
|
|
"""Limit paid orders list loaded on POS frontend to the current day."""
|
|
user_tz_name = self.env.context.get('tz') or self.env.user.tz or 'UTC'
|
|
try:
|
|
user_tz = pytz.timezone(user_tz_name)
|
|
except pytz.UnknownTimeZoneError:
|
|
user_tz = pytz.UTC
|
|
|
|
today_local = fields.Date.to_date(fields.Date.context_today(self))
|
|
today_start_local = datetime.combine(today_local, datetime.min.time())
|
|
today_start_tz = user_tz.localize(today_start_local)
|
|
today_start_utc = today_start_tz.astimezone(pytz.UTC).replace(tzinfo=None)
|
|
|
|
# Restrict domain so only orders from the current day (local timezone start) are returned
|
|
domain = Domain.AND([domain, [('date_order', '>=', today_start_utc)]])
|
|
|
|
return super(PosOrder, self).search_paid_order_ids(config_id, domain, limit, offset)
|
|
|
|
@api.model
|
|
def check_table_has_real_orders(self, table_id, local_order_ids=None):
|
|
"""
|
|
Check whether a restaurant table has real (non-empty, non-cancelled) orders
|
|
on the server, excluding the empty local order IDs known to the calling device.
|
|
|
|
This is a safety guard for the multi-device "Release table" scenario:
|
|
- Device X creates an order on table T and syncs it to the server.
|
|
- Device Y opens table T and sees an empty local order (sync lag).
|
|
- Before allowing Device Y to release (cancel) the empty order, we verify
|
|
with the server that no real orderlines exist for this table.
|
|
|
|
Args:
|
|
table_id (int): The restaurant.table ID to check.
|
|
local_order_ids (list[int]): Server IDs of orders already known to the
|
|
calling device (typically the empty local order).
|
|
|
|
Returns:
|
|
dict:
|
|
- has_real_orders (bool): True if real draft orders with non-zero lines exist.
|
|
- order_count (int): Number of matching server orders found.
|
|
"""
|
|
if local_order_ids is None:
|
|
local_order_ids = []
|
|
|
|
# Find draft (non-finalized) orders for this table that have lines
|
|
domain = [
|
|
('table_id', '=', table_id),
|
|
('state', '=', 'draft'),
|
|
('lines', '!=', False),
|
|
]
|
|
if local_order_ids:
|
|
# Exclude the empty order(s) already known to the calling device so we
|
|
# don't false-positive on an order that this device itself created.
|
|
domain = Domain.AND([domain, [('id', 'not in', local_order_ids)]])
|
|
|
|
orders = self.env['pos.order'].search(domain)
|
|
|
|
# Double-check: at least one line with non-zero qty must exist
|
|
real_orders = orders.filtered(lambda o: any(line.qty != 0 for line in o.lines))
|
|
|
|
return {
|
|
'has_real_orders': bool(real_orders),
|
|
'order_count': len(real_orders),
|
|
}
|
|
|
|
def _process_order(self, order, existing_order):
|
|
"""
|
|
Override to automatically concatenate orderline customer notes into the
|
|
general_customer_note so they are easily visible on the backend.
|
|
"""
|
|
if 'lines' in order:
|
|
line_notes = []
|
|
for line in order['lines']:
|
|
if len(line) >= 3 and isinstance(line[2], dict) and line[2].get('customer_note'):
|
|
note = line[2]['customer_note'].strip()
|
|
if note:
|
|
line_notes.append(note)
|
|
|
|
if line_notes:
|
|
current_note = order.get('general_customer_note') or ""
|
|
existing_parts = [p.strip() for p in current_note.split(" / ")] if current_note else []
|
|
new_notes = [n for n in line_notes if n not in existing_parts]
|
|
|
|
if new_notes:
|
|
combined_notes = " / ".join(new_notes)
|
|
if current_note:
|
|
order['general_customer_note'] = f"{current_note} / {combined_notes}"
|
|
else:
|
|
order['general_customer_note'] = combined_notes
|
|
|
|
return super(PosOrder, self)._process_order(order, existing_order)
|
|
|
|
@api.model
|
|
def sync_from_ui(self, orders):
|
|
"""
|
|
Override to prevent concurrent processing of the same order uuid.
|
|
Uses pg_advisory_xact_lock on the order's uuid to force subsequent
|
|
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
|
|
sorted_orders = sorted(orders, key=lambda x: x.get('uuid') or '')
|
|
for order in sorted_orders:
|
|
uuid = order.get('uuid')
|
|
if uuid:
|
|
# Use PostgreSQL transaction-level advisory lock
|
|
self.env.cr.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (uuid,))
|
|
|
|
return super(PosOrder, self).sync_from_ui(orders)
|
|
|
|
|