Before this commit, if the POS user didn't have access to the first company, an access error would be raised when accessing the self-order page. The steps to reproduce this issue are as follows: 1. Add another company and set up POS self-order. 2. Remove the other company from the Marc user. 3. Open the POS in the new company. 4. Try to access the mobile menu. This would result in an access error. The issue was that the first company existed in the `allowed_company_ids` in the context. To solve this, we need to correctly set the `allowed_company_ids` in the context. opw-3744500 closes odoo/odoo#156357 Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
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).with_context(allowed_company_ids=company.ids)
|
|
|
|
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).with_context(allowed_company_ids=company.ids)
|
|
return pos_config, table
|