diff --git a/addons/pos_online_payment_self_order/tests/test_self_order_frontend.py b/addons/pos_online_payment_self_order/tests/test_self_order_frontend.py index 92803a3aaef..2b942427412 100644 --- a/addons/pos_online_payment_self_order/tests/test_self_order_frontend.py +++ b/addons/pos_online_payment_self_order/tests/test_self_order_frontend.py @@ -28,7 +28,7 @@ class TestSelfOrderFrontendMobile(SelfOrderCommonTest): 'self_order_pay_after': 'each', 'self_order_online_payment_method_id': self.self_order_online_payment_method_id, }) - self.pos_config.open_ui() + self.pos_config.with_user(self.pos_user).open_ui() self.start_tour( self.pos_config._get_self_order_route(), diff --git a/addons/pos_self_order/controllers/entry.py b/addons/pos_self_order/controllers/entry.py index bba59fd5c5a..2f352313831 100644 --- a/addons/pos_self_order/controllers/entry.py +++ b/addons/pos_self_order/controllers/entry.py @@ -8,7 +8,7 @@ from odoo.http import request from odoo.addons.pos_self_order.controllers.utils import ( get_any_pos_config_sudo, - get_table_sudo, + reduce_privilege, ) @@ -31,10 +31,8 @@ class PosQRMenuController(http.Controller): auth="public", website=True, sitemap=True, ) def pos_self_order_start(self, config_id=None, access_token=None, table_identifier=None): - self_order_mode = 'qr_code' - table_infos = False pos_config_sudo = False - pos_config_access_token = False + table_sudo = False if config_id and config_id.isnumeric() and access_token: pos_config_sudo = request.env["pos.config"].sudo().search([ @@ -42,17 +40,23 @@ class PosQRMenuController(http.Controller): ('access_token', '=', access_token)], limit=1) if pos_config_sudo and pos_config_sudo.has_active_session and pos_config_sudo.self_order_table_mode: - self_order_mode = pos_config_sudo.self_order_pay_after - pos_config_access_token = pos_config_sudo.access_token - table_sudo = get_table_sudo(identifier=table_identifier) - table_infos = table_sudo._get_self_order_data() if table_sudo else False + table_sudo = table_identifier and ( + request.env["restaurant.table"] + .sudo() + .search([("identifier", "=", table_identifier), ("active", "=", True)], limit=1) + ) elif config_id and config_id.isnumeric(): pos_config_sudo = request.env["pos.config"].sudo().search([ ("id", "=", config_id), ("self_order_view_mode", "=", True)], limit=1) else: pos_config_sudo = get_any_pos_config_sudo() - if not pos_config_sudo: + company = pos_config_sudo.company_id + user = pos_config_sudo.current_session_id.user_id + pos_config = reduce_privilege(pos_config_sudo, company, user) + table = reduce_privilege(table_sudo, company, user) + + if not pos_config: raise werkzeug.exceptions.NotFound() return request.render( @@ -62,10 +66,10 @@ class PosQRMenuController(http.Controller): **request.env["ir.http"].get_frontend_session_info(), 'currencies': request.env["ir.http"].get_currencies(), 'pos_self_order_data': { - 'self_order_mode': self_order_mode, - 'table': table_infos, - 'access_token': pos_config_access_token, - **pos_config_sudo._get_self_order_data(), + 'self_order_mode': pos_config.self_order_pay_after, + 'table': table._get_self_order_data() if table else False, + 'access_token': pos_config.access_token, + **pos_config._get_self_order_data(), }, "base_url": request.env['pos.session'].get_base_url(), } diff --git a/addons/pos_self_order/controllers/orders.py b/addons/pos_self_order/controllers/orders.py index e24f21ac94e..7ff30a0bc1b 100644 --- a/addons/pos_self_order/controllers/orders.py +++ b/addons/pos_self_order/controllers/orders.py @@ -4,17 +4,17 @@ from datetime import timedelta import uuid from odoo import http, fields, Command from odoo.http import request +from odoo.addons.pos_self_order.controllers.utils import reduce_privilege from werkzeug.exceptions import NotFound, BadRequest, Unauthorized class PosSelfOrderController(http.Controller): @http.route("/pos-self-order/process-new-order", auth="public", type="json", website=True) def process_new_order(self, order, access_token, table_identifier): lines = order.get('lines') - - pos_config_sudo, table_sudo = self._verify_authorization(access_token, table_identifier) - pos_session_sudo = pos_config_sudo.current_session_id - sequence_number = self._get_sequence_number(table_sudo.id, pos_session_sudo.id) - unique_id = self._generate_unique_id(pos_session_sudo.id, table_sudo.id, sequence_number) + pos_config, table = self._verify_authorization(access_token, table_identifier) + pos_session = pos_config.current_session_id + sequence_number = self._get_sequence_number(table.id, pos_session.id) + unique_id = self._generate_unique_id(pos_session.id, table.id, sequence_number) # Create the order without lines and prices computed # We need to remap the order because some required fields are not used in the frontend. @@ -26,11 +26,11 @@ class PosSelfOrderController(http.Controller): 'user_id': request.session.uid, 'sequence_number': sequence_number, 'access_token': uuid.uuid4().hex, - 'pos_session_id': pos_session_sudo.id, - 'table_id': table_sudo.id if table_sudo else False, + 'pos_session_id': pos_session.id, + 'table_id': table.id if table else False, 'partner_id': False, 'creation_date': str(fields.Datetime.now()), - 'fiscal_position_id': pos_config_sudo.default_fiscal_position_id, + 'fiscal_position_id': pos_config.default_fiscal_position_id.id, 'statement_ids': [], 'lines': [], 'amount_tax': 0, @@ -39,36 +39,32 @@ class PosSelfOrderController(http.Controller): 'amount_return': 0, }, 'to_invoice': False, - 'session_id': pos_session_sudo.id, + 'session_id': pos_session.id, } # Save the order in the database to get the id - posted_order_id = request.env['pos.order'].sudo().create_from_ui([order], draft=True)[0].get('id') + posted_order_id = pos_config.env['pos.order'].create_from_ui([order], draft=True)[0].get('id') # Process the lines and get their prices computed - lines = self._process_lines(lines, pos_config_sudo, posted_order_id) + lines = self._process_lines(lines, pos_config, posted_order_id) # Compute the order prices amount_total, amount_untaxed = self._get_order_prices(lines) # Update the order with the computed prices and lines - order_sudo = request.env["pos.order"].sudo().browse(posted_order_id) - order_sudo.write({ + order = pos_config.env["pos.order"].browse(posted_order_id) + order.write({ 'lines': [Command.create(line) for line in lines], 'amount_tax': amount_total - amount_untaxed, 'amount_total': amount_total, }) - return order_sudo._export_for_self_order() + 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_sudo = request.env['pos.config'].sudo().search([('access_token', '=', access_token)], limit=1) - - if not pos_config_sudo or not pos_config_sudo.self_order_table_mode or not pos_config_sudo.has_active_session: - raise Unauthorized("Invalid access token") - - lines = self._process_lines(order.get('lines'), pos_config_sudo, 0) + pos_config = self._verify_pos_config(access_token) + lines = self._process_lines(order.get('lines'), pos_config, 0) amount_total, amount_untaxed = self._get_order_prices(lines) return { @@ -85,88 +81,87 @@ class PosSelfOrderController(http.Controller): def update_existing_order(self, order, access_token, table_identifier): order_id = order.get('id') order_access_token = order.get('access_token') - pos_config_sudo, table_sudo = self._verify_authorization(access_token, table_identifier) + pos_config, table = self._verify_authorization(access_token, table_identifier) + session = pos_config.current_session_id - order_sudo = request.env['pos.order'].sudo().search([ + pos_order = session.order_ids.filtered_domain([ ('id', '=', order_id), ('access_token', '=', order_access_token), - ('table_id', '=', table_sudo.id) + ('table_id', '=', table.id) ]) - if not order_sudo: + if not pos_order: raise Unauthorized("Order not found in the server !") - elif order_sudo.state != 'draft': + elif pos_order.state != 'draft': raise Unauthorized("Order is not in draft state") - lines = self._process_lines(order.get('lines'), pos_config_sudo, order_sudo.id) + lines = self._process_lines(order.get('lines'), pos_config, pos_order.id) for line in lines: if line.get('id'): - line_sudo = order_sudo.lines.filtered(lambda l: l.id == line.get('id')) + order_line = pos_order.lines.browse(line.get('id')) - if line.get('qty') < line_sudo.qty: - line.set('qty', line_sudo.qty) + if line.get('qty') < order_line.qty: + line.set('qty', order_line.qty) - line_sudo.write({ + order_line.write({ **line, }) else: - order_sudo.lines.create(line) + pos_order.lines.create(line) amount_total, amount_untaxed = self._get_order_prices(lines) - order_sudo.write({ + pos_order.write({ 'amount_tax': amount_total - amount_untaxed, 'amount_total': amount_total, }) - return order_sudo._export_for_self_order() + 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, order_access_tokens): - orders_sudo = request.env["pos.order"].sudo().search([ + 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_sudo: + if not orders: raise NotFound("Orders not found") orders = [] - for order in orders_sudo: + for order in orders: orders.append(order._export_for_self_order()) return orders @http.route('/pos-self-order/get-tables', auth='public', type='json', website=True) def get_tables(self, access_token): - 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_order_table_mode or not pos_config_sudo.has_active_session: - raise Unauthorized("Invalid access token") - - tables = pos_config_sudo.floor_ids.table_ids.filtered(lambda t: t.active).read(['id', 'name', 'identifier', 'floor_id']) + 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 - def _process_lines(self, lines, pos_config_sudo, pos_order_id): + def _process_lines(self, lines, pos_config, pos_order_id): newLines = [] - pricelist = request.env['product.pricelist'].sudo().browse(pos_config_sudo.pricelist_id.id) + pricelist = pos_config.pricelist_id for line in lines: - product_sudo = request.env["product.product"].sudo().browse(int(line.get("product_id"))) + product = pos_config.env['product.product'].browse(line.get('product_id')) # todo take into account the price extra - price_unit = pricelist._get_product_price(product_sudo, quantity=1) if pricelist else product_sudo.lst_price + price_unit = pricelist._get_product_price(product, quantity=line.get('qty')) if pricelist else product.lst_price - config_fiscal_pos = pos_config_sudo.default_fiscal_position_id - selected_account_tax = config_fiscal_pos.map_tax(product_sudo.taxes_id) if config_fiscal_pos else product_sudo.taxes_id + config_fiscal_pos = pos_config.default_fiscal_position_id + selected_account_tax = config_fiscal_pos.map_tax(product.taxes_id) if config_fiscal_pos else product.taxes_id tax_results = selected_account_tax.compute_all( price_unit, - pos_config_sudo.currency_id, + pos_config.currency_id, line.get('qty'), - product_sudo, + product, ) newLines.append({ @@ -176,7 +171,7 @@ class PosSelfOrderController(http.Controller): 'price_extra': 0, 'id': line.get('id'), 'order_id': pos_order_id, - 'tax_ids': product_sudo.taxes_id, + 'tax_ids': product.taxes_id, 'uuid': line.get('uuid'), 'product_id': line.get('product_id'), 'qty': line.get('qty'), @@ -212,14 +207,29 @@ class PosSelfOrderController(http.Controller): return (order_sudo.sequence_number + 1) or 1 - def _verify_authorization(self, access_token, table_identifier): - table_sudo = request.env["restaurant.table"].sudo().search([('identifier', '=', table_identifier)], limit=1) + 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_order_table_mode 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 + return reduce_privilege(pos_config_sudo, company, user) + def _verify_authorization(self, access_token, table_identifier): + """ + 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. + """ + table_sudo = request.env["restaurant.table"].sudo().search([('identifier', '=', table_identifier)], limit=1) if not table_sudo: raise Unauthorized("Table not found") - return pos_config_sudo, table_sudo + pos_config = self._verify_pos_config(access_token) + company = pos_config.company_id + user = pos_config.current_session_id.user_id + table = reduce_privilege(table_sudo, company, user) + return pos_config, table diff --git a/addons/pos_self_order/controllers/utils.py b/addons/pos_self_order/controllers/utils.py index 223d93f4335..8aed1a0c58c 100644 --- a/addons/pos_self_order/controllers/utils.py +++ b/addons/pos_self_order/controllers/utils.py @@ -2,7 +2,6 @@ import werkzeug -from typing import Optional from odoo.http import request from odoo.addons.pos_self_order.models.pos_config import PosConfig @@ -16,14 +15,18 @@ def get_any_pos_config_sudo() -> PosConfig: request.env["pos.config"].sudo().search([("self_order_view_mode", "=", True)], limit=1) ) or _raise(werkzeug.exceptions.NotFound()) - -def get_table_sudo(identifier): - return identifier and ( - request.env["restaurant.table"] - .sudo() - .search([("identifier", "=", identifier), ("active", "=", True)], limit=1) - ) - - def _raise(e): raise e + +def reduce_privilege(record_sudo, company, user=None): + """ + Returns a record with reduced privileges based on company and user. + If user is not provided, we keep the sudo privilege, but still, the record + will be scoped to the company. + """ + if record_sudo: + if user: + return record_sudo.sudo(False).with_company(company).with_user(user) + else: + return record_sudo.with_company(company) + return None diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py index e9e1770ce98..82177da1e38 100644 --- a/addons/pos_self_order/models/pos_config.py +++ b/addons/pos_self_order/models/pos_config.py @@ -144,7 +144,6 @@ class PosConfig(models.Model): self.ensure_one() return ( self.env["pos_self_order.custom_link"] - .sudo() .search_read( [ "|", @@ -163,7 +162,6 @@ class PosConfig(models.Model): self.ensure_one() return ( self.env["product.product"] - .sudo() .search( [ ("available_in_pos", "=", True), @@ -186,7 +184,7 @@ class PosConfig(models.Model): "show_prices_with_tax_included": self.iface_tax_included == "total", "custom_links": self._get_self_order_custom_links(), "products": self._get_available_products()._get_self_order_data(self), - "pos_category": self.env['pos.category'].sudo().search_read(fields=["name", "sequence"], order="sequence"), + "pos_category": self.env['pos.category'].search_read(fields=["name", "sequence"], order="sequence"), "has_active_session": self.has_active_session, } diff --git a/addons/pos_self_order/models/product_product.py b/addons/pos_self_order/models/product_product.py index b4e2c4dae19..169db5f1ba5 100644 --- a/addons/pos_self_order/models/product_product.py +++ b/addons/pos_self_order/models/product_product.py @@ -36,7 +36,7 @@ class ProductProduct(models.Model): self.ensure_one() attributes = self._filter_applicable_attributes( - self.env["pos.session"].sudo()._get_attributes_by_ptal_id() + self.env["pos.session"]._get_attributes_by_ptal_id() ) return self._add_price_info_to_attributes( attributes, diff --git a/addons/pos_self_order/static/src/components/product_card/product_card.js b/addons/pos_self_order/static/src/components/product_card/product_card.js index da8439f9032..908f0cf6807 100644 --- a/addons/pos_self_order/static/src/components/product_card/product_card.js +++ b/addons/pos_self_order/static/src/components/product_card/product_card.js @@ -32,10 +32,4 @@ export class ProductCard extends Component { const productId = this.props.line.product_id; return `${this.props.line.qty} x ${this.selfOrder.productByIds[productId].prices}`; } - - getTotalPriceString() { - const productPriceWithTax = this.props.product.prices; - const quantity = this.props.line?.qty ? this.props.line.qty : 1; - return this.selfOrder.formatMonetary(productPriceWithTax * quantity); - } } diff --git a/addons/pos_self_order/static/src/components/product_card/product_card.xml b/addons/pos_self_order/static/src/components/product_card/product_card.xml index cbf534cc999..5635e31a05a 100644 --- a/addons/pos_self_order/static/src/components/product_card/product_card.xml +++ b/addons/pos_self_order/static/src/components/product_card/product_card.xml @@ -14,7 +14,7 @@ t-if="quantityInCart and selfOrder.ordering" t-esc="`${quantityInCart}x`" /> - +
diff --git a/addons/pos_self_order/static/src/self_order_service.js b/addons/pos_self_order/static/src/self_order_service.js index 0b9028ee46e..87d56bf52e9 100644 --- a/addons/pos_self_order/static/src/self_order_service.js +++ b/addons/pos_self_order/static/src/self_order_service.js @@ -178,6 +178,7 @@ export class SelfOrder { try { const orders = await this.rpc(`/pos-self-order/get-orders/`, { + access_token: this.access_token, order_access_tokens: accessTokens, }); diff --git a/addons/pos_self_order/tests/self_order_common_test.py b/addons/pos_self_order/tests/self_order_common_test.py index 96475a10039..bb5d6ce236f 100644 --- a/addons/pos_self_order/tests/self_order_common_test.py +++ b/addons/pos_self_order/tests/self_order_common_test.py @@ -3,12 +3,48 @@ import odoo.tests +from odoo import Command + @odoo.tests.tagged("post_install", "-at_install") class SelfOrderCommonTest(odoo.tests.HttpCase): browser_size = "375x667" touch_enabled = True + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.pos_user = cls.env['res.users'].create({ + 'name': 'POS User', + 'login': 'pos_user', + 'password': 'pos_user', + 'groups_id': [ + (4, cls.env.ref('base.group_user').id), + (4, cls.env.ref('point_of_sale.group_pos_user').id), + ], + }) + + def _add_tax_to_product_from_different_company(self): + new_company = self.env['res.company'].create({ + 'name': 'Test Company', + 'currency_id': self.env.ref('base.USD').id, + 'country_id': self.env.ref('base.us').id, + }) + + new_tax = self.env['account.tax'].with_company(new_company).create({ + 'name': 'Tax that should not be used', + 'amount': 50, + 'amount_type': 'percent', + 'tax_group_id': self.env['account.tax.group'].with_company(new_company).create({ + 'name': 'Tax Group that should not be used', + }).id, + 'company_id': new_company.id, + }) + + self.env['product.product'].search([]).with_company(new_company).write({ + 'taxes_id': [Command.link(id) for id in new_tax.ids], + }) + def setUp(self): super().setUp() self.pos_config = self.env["pos.config"].create( @@ -28,3 +64,7 @@ class SelfOrderCommonTest(odoo.tests.HttpCase): 'amount': 15, 'amount_type': 'percent', }) + + # A new tax is added to each product and this tax is from a different company. + # This is important in the test because the added tax should not be used in the tour. + self._add_tax_to_product_from_different_company() diff --git a/addons/pos_self_order/tests/test_frontend.py b/addons/pos_self_order/tests/test_frontend.py index dcf21602e91..e167eea55f9 100644 --- a/addons/pos_self_order/tests/test_frontend.py +++ b/addons/pos_self_order/tests/test_frontend.py @@ -19,7 +19,7 @@ class TestFrontendMobile(SelfOrderCommonTest): def test_self_order_pay_after_meal_tour(self): self.pos_config.self_order_table_mode = True self.pos_config.self_order_pay_after = "meal" - self.pos_config.open_ui() + self.pos_config.with_user(self.pos_user).open_ui() self.start_tour( self.pos_config._get_self_order_route(), "self_order_after_meal_cart_tour",