From e628b6507f60e11e360dcdffbb68303d151cf7a9 Mon Sep 17 00:00:00 2001 From: Joseph Caburnay Date: Mon, 7 Aug 2023 08:59:00 +0000 Subject: [PATCH] [FIX] pos_{online_payment_}self_order: price based on company taxes **Steps to reproduce:** - Install pos_self_order. - Create a product with a price of any price, say 100. - Assign a tax of 10% to the product. - Create a new company and switch to it. - With the same product, add a new tax, say 20%. - Go back to the original company. - Open a bar (restaurant pos.config) that allows self order which also loads the product. - Open self order page and add the product. - [BUG] The product's price is not only 10%-taxed, but also 20%-taxed. **Explanation and fix** The issue is caused by use of sudo almost everywhere in the context of pos_self_order. This commit removes/reduces this use of sudo in many places and contextualize the records involved in the calculation such as pos.config, product.product, etc. to be the ones of the company and the user who opened the current pos.session. After this changes, only the taxes that belong to the company of the pos.config record are used in the price and tax calculations. closes odoo/odoo#131140 X-original-commit: 7ce7f3e21ad8f457742e27f88b7146c7d8fff5f3 Signed-off-by: David Monnom (moda) Signed-off-by: Joseph Caburnay (jcb) --- .../tests/test_self_order_frontend.py | 2 +- addons/pos_self_order/controllers/entry.py | 30 +++-- addons/pos_self_order/controllers/orders.py | 122 ++++++++++-------- addons/pos_self_order/controllers/utils.py | 23 ++-- addons/pos_self_order/models/pos_config.py | 4 +- .../pos_self_order/models/product_product.py | 2 +- .../components/product_card/product_card.js | 6 - .../components/product_card/product_card.xml | 3 +- .../static/src/self_order_service.js | 1 + .../tests/self_order_common_test.py | 40 ++++++ addons/pos_self_order/tests/test_frontend.py | 2 +- 11 files changed, 142 insertions(+), 93 deletions(-) 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",