Files
Pedram (pebr) e7d270063f [FIX] pos_self_order: prevent multi-company access error
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>
2024-03-08 09:42:52 +00:00

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