[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) <moda@odoo.com>
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
Joseph Caburnay
2023-08-08 08:56:25 +02:00
parent 4a38bf2927
commit e628b6507f
11 changed files with 142 additions and 93 deletions
@@ -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(),
+17 -13
View File
@@ -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(),
}
+66 -56
View File
@@ -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
+13 -10
View File
@@ -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
+1 -3
View File
@@ -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,
}
@@ -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,
@@ -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);
}
}
@@ -14,7 +14,7 @@
t-if="quantityInCart and selfOrder.ordering"
t-esc="`${quantityInCart}x`"
/>
<span class="card-text small" t-esc="getTotalPriceString()" />
<span class="card-text small" t-esc="selfOrder.formatMonetary(props.product.prices)" />
</div>
<p
t-if="props.product.description_sale"
@@ -27,7 +27,6 @@
t-attf-src="/menu/get-image/{{ props.product.id }}"
alt="Product image"
loading="lazy"
onerror="this.remove()"
/>
<span t-if="quantityInCart and selfOrder.ordering" class="position-absolute top-0 start-0 h-100 border-start border-5 border-primary" />
</div>
@@ -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,
});
@@ -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()
+1 -1
View File
@@ -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",