343 lines
17 KiB
Python
343 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
import re
|
|
import uuid
|
|
from datetime import timedelta
|
|
from odoo import http, fields
|
|
from odoo.http import request
|
|
from odoo.tools import float_round
|
|
from werkzeug.exceptions import NotFound, BadRequest, Unauthorized
|
|
|
|
class PosSelfOrderController(http.Controller):
|
|
@http.route("/pos-self-order/process-new-order/<device_type>/", auth="public", type="json", website=True)
|
|
def process_new_order(self, order, access_token, table_identifier, device_type):
|
|
lines = order.get('lines')
|
|
is_take_away = order.get('take_away')
|
|
pos_config, table = self._verify_authorization(access_token, table_identifier, is_take_away)
|
|
pos_session = pos_config.current_session_id
|
|
ir_sequence_session = pos_config.env['ir.sequence'].with_context(company_id=pos_config.company_id.id).next_by_code(f'pos.order_{pos_session.id}')
|
|
|
|
sequence_number = re.findall(r'\d+', ir_sequence_session)[0]
|
|
order_reference = self._generate_unique_id(pos_session.id, pos_config.id, sequence_number, device_type)
|
|
|
|
fiscal_position = (
|
|
pos_config.self_ordering_alternative_fp_id
|
|
if is_take_away
|
|
else pos_config.default_fiscal_position_id
|
|
)
|
|
|
|
# Create the order without lines and prices computed
|
|
# We need to remap the order because some required fields are not used in the frontend.
|
|
order = {
|
|
'data': {
|
|
'name': order_reference,
|
|
'sequence_number': sequence_number,
|
|
'uuid': order.get('uuid'),
|
|
'take_away': order.get('take_away'),
|
|
'user_id': request.session.uid,
|
|
'access_token': uuid.uuid4().hex,
|
|
'pos_session_id': pos_session.id,
|
|
'table_id': table.id if table else False,
|
|
'partner_id': False,
|
|
'date_order': str(fields.Datetime.now()),
|
|
'fiscal_position_id': fiscal_position.id,
|
|
'statement_ids': [],
|
|
'lines': [],
|
|
'amount_tax': 0,
|
|
'amount_total': 0,
|
|
'amount_paid': 0,
|
|
'amount_return': 0,
|
|
'table_stand_number': order.get('table_stand_number'),
|
|
'ticket_code': order.get('ticket_code'),
|
|
},
|
|
'to_invoice': False,
|
|
'session_id': pos_session.id,
|
|
}
|
|
|
|
# Save the order in the database to get the id
|
|
posted_order_id = pos_config.env['pos.order'].with_context(from_self=True).create_from_ui([order], draft=True)[0].get('id')
|
|
|
|
# Process the lines and get their prices computed
|
|
lines = self._process_lines(lines, pos_config, posted_order_id, is_take_away)
|
|
|
|
# Compute the order prices
|
|
amount_total, amount_untaxed = self._get_order_prices(lines)
|
|
|
|
# Update the order with the computed prices and lines
|
|
order = pos_config.env["pos.order"].browse(posted_order_id)
|
|
|
|
classic_lines = []
|
|
combo_lines = []
|
|
for line in lines:
|
|
if line["combo_parent_uuid"]:
|
|
combo_lines.append(line)
|
|
else:
|
|
classic_lines.append(line)
|
|
|
|
# combo lines must be created after classic_line, as they need the classic line identifier
|
|
# use user admin to avoid access rights issues
|
|
lines = pos_config.env['pos.order.line'].with_user(pos_config.self_ordering_default_user_id).create(classic_lines)
|
|
lines += pos_config.env['pos.order.line'].with_user(pos_config.self_ordering_default_user_id).create(combo_lines)
|
|
|
|
order.write({
|
|
'lines': lines,
|
|
'state': 'paid' if amount_total == 0 else 'draft',
|
|
'amount_tax': amount_total - amount_untaxed,
|
|
'amount_total': amount_total,
|
|
})
|
|
|
|
order.send_table_count_notification(order.table_id)
|
|
return order._export_for_self_order()
|
|
|
|
@http.route('/pos-self-order/get-orders-taxes', auth='public', type='json', website=True)
|
|
def get_order_taxes(self, order, access_token):
|
|
pos_config = self._verify_pos_config(access_token)
|
|
lines = self._process_lines(order.get('lines'), pos_config, 0, order.get('take_away'))
|
|
amount_total, amount_untaxed = self._get_order_prices(lines)
|
|
|
|
return {
|
|
'lines': [{
|
|
'uuid': line.get('uuid'),
|
|
'price_unit': line.get('price_unit'),
|
|
'price_extra': line.get('price_extra'),
|
|
'price_subtotal': line.get('price_subtotal'),
|
|
'price_subtotal_incl': line.get('price_subtotal_incl'),
|
|
} for line in lines],
|
|
'amount_total': amount_total,
|
|
'amount_tax': amount_total - amount_untaxed,
|
|
}
|
|
|
|
@http.route('/pos-self-order/update-existing-order', auth="public", type="json", website=True)
|
|
def update_existing_order(self, order, access_token, table_identifier):
|
|
order_id = order.get('id')
|
|
order_access_token = order.get('access_token')
|
|
pos_config, table = self._verify_authorization(access_token, table_identifier, order.get('take_away'))
|
|
session = pos_config.current_session_id
|
|
|
|
pos_order = session.order_ids.filtered_domain([
|
|
('id', '=', order_id),
|
|
('access_token', '=', order_access_token),
|
|
])
|
|
|
|
if not pos_order:
|
|
raise Unauthorized("Order not found in the server !")
|
|
elif pos_order.state != 'draft':
|
|
raise Unauthorized("Order is not in draft state")
|
|
|
|
lines = self._process_lines(order.get('lines'), pos_config, pos_order.id, order.get('take_away'))
|
|
for line in lines:
|
|
if line.get('id'):
|
|
# we need to find by uuid because each time we update the order, id of orderlines changed.
|
|
order_line = pos_order.lines.filtered(lambda l: l.uuid == line.get('uuid'))
|
|
|
|
if line.get('qty') < order_line.qty:
|
|
line.set('qty', order_line.qty)
|
|
|
|
if order_line:
|
|
order_line.write({
|
|
**line,
|
|
})
|
|
else:
|
|
pos_order.lines.create(line)
|
|
|
|
amount_total, amount_untaxed = self._get_order_prices(lines)
|
|
pos_order.write({
|
|
'amount_tax': amount_total - amount_untaxed,
|
|
'amount_total': amount_total,
|
|
'table_id': table if table else False,
|
|
'table_stand_number': order.get('table_stand_number'),
|
|
})
|
|
|
|
pos_order.send_table_count_notification(pos_order.table_id)
|
|
return pos_order._export_for_self_order()
|
|
|
|
@http.route('/pos-self-order/get-orders', auth='public', type='json', website=True)
|
|
def get_orders_by_access_token(self, access_token, order_access_tokens):
|
|
pos_config = self._verify_pos_config(access_token)
|
|
session = pos_config.current_session_id
|
|
orders = session.order_ids.filtered_domain([
|
|
("access_token", "in", order_access_tokens),
|
|
("date_order", ">=", fields.Datetime.now() - timedelta(days=7)),
|
|
])
|
|
|
|
if not orders:
|
|
raise NotFound("Orders not found")
|
|
|
|
orders_for_ui = []
|
|
for order in orders:
|
|
orders_for_ui.append(order._export_for_self_order())
|
|
|
|
return orders_for_ui
|
|
|
|
@http.route('/pos-self-order/get-tables', auth='public', type='json', website=True)
|
|
def get_tables(self, access_token):
|
|
pos_config = self._verify_pos_config(access_token)
|
|
tables = pos_config.floor_ids.table_ids.filtered(lambda t: t.active).read(['id', 'name', 'identifier', 'floor_id'])
|
|
|
|
for table in tables:
|
|
table['floor_name'] = table.get('floor_id')[1]
|
|
|
|
return tables
|
|
|
|
|
|
@http.route('/kiosk/payment/<int:pos_config_id>/<device_type>', auth='public', type='json', website=True)
|
|
def pos_self_order_kiosk_payment(self, pos_config_id, order, payment_method_id, access_token, device_type):
|
|
pos_config = self._verify_pos_config(access_token)
|
|
order_dict = self.process_new_order(order, access_token, None, device_type)
|
|
|
|
if not order_dict.get('id'):
|
|
raise BadRequest("Something went wrong")
|
|
|
|
# access_token verified in process_new_order
|
|
order_sudo = pos_config.env['pos.order'].browse(order_dict.get('id'))
|
|
payment_method_sudo = pos_config.env["pos.payment.method"].browse(payment_method_id)
|
|
if not order_sudo or not payment_method_sudo or payment_method_sudo not in order_sudo.config_id.payment_method_ids:
|
|
raise NotFound("Order or payment method not found")
|
|
|
|
status = payment_method_sudo._payment_request_from_kiosk(order_sudo)
|
|
|
|
if not status:
|
|
raise BadRequest("Something went wrong")
|
|
|
|
return {'order': order_sudo._export_for_self_order(), 'payment_status': status}
|
|
|
|
def _process_lines(self, lines, pos_config, pos_order_id, take_away=False):
|
|
appended_uuid = []
|
|
newLines = []
|
|
pricelist = pos_config.pricelist_id
|
|
sale_price_digits = pos_config.env['decimal.precision'].precision_get('Product Price')
|
|
|
|
combo_line_ids = [line['combo_line_id'] for line in lines if line.get('combo_line_id')]
|
|
combo_lines = pos_config.env['pos.combo.line'].search([('id', 'in', combo_line_ids)])
|
|
attribute_value_ids = sum([line.get('attribute_value_ids', []) for line in lines], [])
|
|
fetched_attributes = pos_config.env['product.template.attribute.value'].search([('id', 'in', attribute_value_ids)])
|
|
|
|
fiscal_pos = pos_config.default_fiscal_position_id
|
|
|
|
if take_away and pos_config.self_ordering_alternative_fp_id:
|
|
fiscal_pos = pos_config.self_ordering_alternative_fp_id
|
|
|
|
for line in lines:
|
|
if line.get('uuid') in appended_uuid or not line.get('product_id'):
|
|
continue
|
|
|
|
line_qty = line.get('qty')
|
|
product = pos_config.env['product.product'].browse(int(line.get('product_id')))
|
|
lst_price = pricelist._get_product_price(product, quantity=line_qty) if pricelist else product.lst_price
|
|
selected_attributes = fetched_attributes.browse(line.get('attribute_value_ids', []))
|
|
lst_price += sum([attr.price_extra for attr in selected_attributes])
|
|
|
|
children = [l for l in lines if l.get('combo_parent_uuid') == line.get('uuid')]
|
|
pos_combo_lines = combo_lines.browse([child.get('combo_line_id') for child in children])
|
|
|
|
if len(children) > 0:
|
|
original_total = sum(pos_combo_lines.mapped("combo_id.base_price"))
|
|
remaining_total = lst_price
|
|
factor = lst_price / original_total
|
|
|
|
for i, child in enumerate(children):
|
|
child_product = pos_config.env['product.product'].browse(int(child.get('product_id')))
|
|
pos_combo_line = pos_combo_lines.browse(child.get('combo_line_id'))
|
|
price_unit = float_round(pos_combo_line.combo_id.base_price * factor, precision_digits=sale_price_digits)
|
|
remaining_total -= price_unit
|
|
if i == len(children) - 1:
|
|
price_unit += remaining_total
|
|
|
|
selected_attributes = fetched_attributes.browse(child.get('attribute_value_ids', []))
|
|
price_unit += pos_combo_line.combo_price + sum([attr.price_extra for attr in selected_attributes])
|
|
|
|
price_unit_fp = child_product._get_price_unit_after_fp(price_unit, pos_config.currency_id, fiscal_pos)
|
|
taxes = fiscal_pos.map_tax(child_product.taxes_id) if fiscal_pos else child_product.taxes_id
|
|
pdetails = taxes.compute_all(price_unit_fp, pos_config.currency_id, line_qty, child_product)
|
|
|
|
newLines.append({
|
|
'price_unit': price_unit_fp,
|
|
'price_subtotal': pdetails.get('total_excluded'),
|
|
'price_subtotal_incl': pdetails.get('total_included'),
|
|
'custom_attribute_value_ids': [[0, 0, cAttr] for cAttr in child.get('custom_attribute_value_ids')] if child.get('custom_attribute_value_ids') else [],
|
|
'id': child.get('id'),
|
|
'order_id': pos_order_id,
|
|
'tax_ids': child_product.taxes_id,
|
|
'uuid': child.get('uuid'),
|
|
'product_id': child.get('product_id'),
|
|
'qty': child.get('qty'),
|
|
'customer_note': child.get('customer_note'),
|
|
'attribute_value_ids': child.get('attribute_value_ids') or [],
|
|
'full_product_name': child.get('full_product_name'),
|
|
'combo_parent_uuid': child.get('combo_parent_uuid'),
|
|
'combo_id': child.get('combo_id'),
|
|
})
|
|
appended_uuid.append(child.get('uuid'))
|
|
|
|
lst_price = 0
|
|
|
|
price_unit_fp = product._get_price_unit_after_fp(lst_price, pos_config.currency_id, fiscal_pos)
|
|
taxes_after_fp = fiscal_pos.map_tax(product.taxes_id) if fiscal_pos else product.taxes_id
|
|
pdetails = taxes_after_fp.compute_all(price_unit_fp, pos_config.currency_id, line_qty, product)
|
|
|
|
newLines.append({
|
|
'price_unit': price_unit_fp,
|
|
'price_subtotal': pdetails.get('total_excluded'),
|
|
'price_subtotal_incl': pdetails.get('total_included'),
|
|
'id': line.get('id'),
|
|
'order_id': pos_order_id,
|
|
'tax_ids': product.taxes_id,
|
|
'uuid': line.get('uuid'),
|
|
'product_id': line.get('product_id'),
|
|
'qty': line_qty,
|
|
'customer_note': line.get('customer_note'),
|
|
'attribute_value_ids': line.get('attribute_value_ids') or [],
|
|
'custom_attribute_value_ids': [[0, 0, cAttr] for cAttr in line.get('custom_attribute_value_ids')] if line.get('custom_attribute_value_ids') else [],
|
|
'full_product_name': line.get('full_product_name'),
|
|
'combo_parent_uuid': line.get('combo_parent_uuid'),
|
|
'combo_id': line.get('combo_id'),
|
|
})
|
|
appended_uuid.append(line.get('uuid'))
|
|
|
|
return newLines
|
|
|
|
def _get_order_prices(self, lines):
|
|
amount_untaxed = sum([line.get('price_subtotal') for line in lines])
|
|
amount_total = sum([line.get('price_subtotal_incl') for line in lines])
|
|
return amount_total, amount_untaxed
|
|
|
|
# The first part will be the session_id of the order.
|
|
# The second part will be the table_id of the order.
|
|
# Last part the sequence number of the order.
|
|
# INFO: This is allow a maximum of 999 tables and 9999 orders per table, so about ~1M orders per session.
|
|
# Example: 'Self-Order 00001-001-0001'
|
|
def _generate_unique_id(self, pos_session_id, config_id, sequence_number, device_type):
|
|
first_part = "{:05d}".format(int(pos_session_id))
|
|
second_part = "{:03d}".format(int(config_id))
|
|
third_part = "{:04d}".format(int(sequence_number))
|
|
|
|
device = "Kiosk" if device_type == "kiosk" else "Self-Order"
|
|
return f"{device} {first_part}-{second_part}-{third_part}"
|
|
|
|
def _verify_pos_config(self, access_token):
|
|
"""
|
|
Finds the pos.config with the given access_token and returns a record with reduced privileges.
|
|
The record is has no sudo access and is in the context of the record's company and current pos.session's user.
|
|
"""
|
|
pos_config_sudo = request.env['pos.config'].sudo().search([('access_token', '=', access_token)], limit=1)
|
|
if not pos_config_sudo or (not pos_config_sudo.self_ordering_mode == 'mobile' and not pos_config_sudo.self_ordering_mode == 'kiosk') or not pos_config_sudo.has_active_session:
|
|
raise Unauthorized("Invalid access token")
|
|
company = pos_config_sudo.company_id
|
|
user = pos_config_sudo.current_session_id.user_id or pos_config_sudo.self_ordering_default_user_id
|
|
return pos_config_sudo.sudo(False).with_company(company).with_user(user)
|
|
|
|
def _verify_authorization(self, access_token, table_identifier, take_away):
|
|
"""
|
|
Similar to _verify_pos_config but also looks for the restaurant.table of the given identifier.
|
|
The restaurant.table record is also returned with reduced privileges.
|
|
"""
|
|
pos_config = self._verify_pos_config(access_token)
|
|
table_sudo = request.env["restaurant.table"].sudo().search([('identifier', '=', table_identifier)], limit=1)
|
|
|
|
if not table_sudo and not pos_config.self_ordering_mode == 'kiosk' and pos_config.self_ordering_service_mode == 'table' and not take_away:
|
|
raise Unauthorized("Table not found")
|
|
|
|
company = pos_config.company_id
|
|
user = pos_config.current_session_id.user_id or pos_config.self_ordering_default_user_id
|
|
table = table_sudo.sudo(False).with_company(company).with_user(user)
|
|
return pos_config, table
|