From b81a049a4e76bfdb5d0beef29a50391f7cd1c2c4 Mon Sep 17 00:00:00 2001 From: "Theo VINCENT (thvi)" Date: Tue, 18 Jul 2023 09:13:17 +0000 Subject: [PATCH] [ADD] pos_online_payment_self_order, *: add module *: pos_self_order Adds online payment support for self-order flow in the Point of Sale app. - Adds a configurable online payment method specific to the self-order flow, which is optional in "Pay after meal" mode, required in "Pay after each" mode. The POS config of a restaurant can use 2 different online payment methods, one for the frontend cashier flow, and another for the self-order flow, or can use the same for both. By default, if an online payment method is configured for the self-order flow, any new order paid online will be paid with that payment method. But if the cashier requests an online payment with a different online payment method, that payment method will be used for that order during the time the cashier keeps his online payment popup open (without cancelling the online payment) (after cancelling the online payment, the order could potentially still be paid online, and if so it will use the online payment method of the self-order if one is configured, otherwise the online payment method of the cashier flow). - Allows the customer to open the online payment page of an order from the self-order UI. In "Pay after meal" mode, if an online payment method is configured, the "Pay" button is displayed when the customer order is saved on the server, otherwise the "Order" button is displayed (even when the order has been sent to the server and then modified without sending it again). Otherwise, if no online payment method is configured, the "Pay at cashier" unclickable button is displayed. In "Pay after each" mode, the "Pay" button is displayed everytime. - Replaces the self_order_after_each_cart_tour test. closes odoo/odoo#127869 Task-id: 3171698 Related: odoo/enterprise#44055 Signed-off-by: Robin Heinz (rhe) --- .../pos_online_payment_self_order/__init__.py | 4 + .../__manifest__.py | 25 ++++++ .../models/__init__.py | 6 ++ .../models/pos_config.py | 34 ++++++++ .../models/pos_order.py | 68 ++++++++++++++++ .../models/res_config_settings.py | 9 +++ .../static/src/pages/order_cart/order_cart.js | 71 +++++++++++++++++ .../static/src/self_order_service.js | 17 ++++ .../tours/self_order_after_each_cart_tour.js | 19 +++++ .../tests/__init__.py | 4 + .../tests/test_self_order_frontend.py | 39 +++++++++ .../views/res_config_settings_views.xml | 17 ++++ addons/pos_self_order/controllers/entry.py | 1 + addons/pos_self_order/models/pos_config.py | 2 +- .../models/res_config_settings.py | 4 +- .../static/src/components/lines/lines.js | 5 +- .../pos_self_order/static/src/models/order.js | 5 +- .../static/src/pages/order_cart/order_cart.js | 5 +- .../src/pages/order_cart/order_cart.xml | 7 +- .../tours/self_order_after_each_cart_tour.js | 79 ------------------- .../tours/self_order_after_meal_cart_tour.js | 4 +- .../tests/self_order_common_test.py | 30 +++++++ addons/pos_self_order/tests/test_frontend.py | 36 +-------- 23 files changed, 362 insertions(+), 129 deletions(-) create mode 100644 addons/pos_online_payment_self_order/__init__.py create mode 100644 addons/pos_online_payment_self_order/__manifest__.py create mode 100644 addons/pos_online_payment_self_order/models/__init__.py create mode 100644 addons/pos_online_payment_self_order/models/pos_config.py create mode 100644 addons/pos_online_payment_self_order/models/pos_order.py create mode 100644 addons/pos_online_payment_self_order/models/res_config_settings.py create mode 100644 addons/pos_online_payment_self_order/static/src/pages/order_cart/order_cart.js create mode 100644 addons/pos_online_payment_self_order/static/src/self_order_service.js create mode 100644 addons/pos_online_payment_self_order/static/tests/tours/self_order_after_each_cart_tour.js create mode 100644 addons/pos_online_payment_self_order/tests/__init__.py create mode 100644 addons/pos_online_payment_self_order/tests/test_self_order_frontend.py create mode 100644 addons/pos_online_payment_self_order/views/res_config_settings_views.xml delete mode 100644 addons/pos_self_order/static/tests/tours/self_order_after_each_cart_tour.js create mode 100644 addons/pos_self_order/tests/self_order_common_test.py diff --git a/addons/pos_online_payment_self_order/__init__.py b/addons/pos_online_payment_self_order/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/pos_online_payment_self_order/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/pos_online_payment_self_order/__manifest__.py b/addons/pos_online_payment_self_order/__manifest__.py new file mode 100644 index 00000000000..38c5dc42cda --- /dev/null +++ b/addons/pos_online_payment_self_order/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'POS Self-Order / Online Payment', + 'category': 'Sales/Point of Sale', + 'summary': 'Support online payment in self-order', + 'version': '1.0', + 'depends': ['pos_online_payment', 'pos_self_order'], + 'data': [ + 'views/res_config_settings_views.xml', + ], + 'installable': True, + 'auto_install': True, + 'assets': { + 'pos_self_order.assets_self_order': [ + 'pos_online_payment_self_order/static/src/**/*', + ], + 'pos_self_order.assets_tests': [ + 'pos_online_payment_self_order/static/tests/tours/**/*', + ], + }, + 'license': 'LGPL-3', +} diff --git a/addons/pos_online_payment_self_order/models/__init__.py b/addons/pos_online_payment_self_order/models/__init__.py new file mode 100644 index 00000000000..1ef21f4c978 --- /dev/null +++ b/addons/pos_online_payment_self_order/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import pos_config +from . import pos_order +from . import res_config_settings diff --git a/addons/pos_online_payment_self_order/models/pos_config.py b/addons/pos_online_payment_self_order/models/pos_config.py new file mode 100644 index 00000000000..21573bdfc6d --- /dev/null +++ b/addons/pos_online_payment_self_order/models/pos_config.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from typing import Dict +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class PosConfig(models.Model): + _inherit = 'pos.config' + + self_order_online_payment_method_id = fields.Many2one('pos.payment.method', string='Online payment method', help="The online payment method to use when a customer pays a self-order online.", domain=[('is_online_payment', '=', True)], compute='_compute_self_order_online_payment_method_id', store=True, readonly=False) + + @api.constrains('self_order_online_payment_method_id') + def _check_self_order_online_payment_method_id(self): + if any(config.self_order_online_payment_method_id and not config.self_order_online_payment_method_id._get_online_payment_providers(config.id, error_if_invalid=True) for config in self): + raise ValidationError(_("The online payment method used for self-order in a POS config must have at least one published payment provider supporting the currency of that POS config.")) + + @api.constrains('self_order_table_mode', 'self_order_pay_after', 'self_order_online_payment_method_id') + def _check_self_order_pay_after_each(self): + if any(config.self_order_table_mode and config.self_order_pay_after == 'each' and not config.self_order_online_payment_method_id for config in self): + raise ValidationError(_("The POS self-order mode with payment after each order requires an online payment method to be configured.")) + + @api.depends('company_id', 'self_order_table_mode', 'self_order_pay_after', 'self_order_online_payment_method_id') + def _compute_self_order_online_payment_method_id(self): + for config in self: + if not config.self_order_table_mode: + config.self_order_online_payment_method_id = False + elif config.self_order_pay_after == 'each' and (not config.self_order_online_payment_method_id or not config.self_order_online_payment_method_id.is_online_payment): + config.self_order_online_payment_method_id = self.env['pos.payment.method'].sudo()._get_or_create_online_payment_method(config.company_id.id, config.id) + + def _get_self_order_data(self) -> Dict: + res = super()._get_self_order_data() + res['has_self_order_online_payment_method'] = bool(self.self_order_online_payment_method_id) + return res diff --git a/addons/pos_online_payment_self_order/models/pos_order.py b/addons/pos_online_payment_self_order/models/pos_order.py new file mode 100644 index 00000000000..6dc2e325895 --- /dev/null +++ b/addons/pos_online_payment_self_order/models/pos_order.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api, tools +from odoo.osv import expression + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + use_self_order_online_payment = fields.Boolean(compute='_compute_use_self_order_online_payment', store=True, readonly=True) + + @api.depends('config_id.self_order_online_payment_method_id') + def _compute_use_self_order_online_payment(self): + for order in self: + order.use_self_order_online_payment = bool(order.config_id.self_order_online_payment_method_id) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if 'use_self_order_online_payment' not in vals or vals['use_self_order_online_payment']: + session = self.env['pos.session'].browse(vals['session_id']) + config = session.config_id + vals['use_self_order_online_payment'] = bool(config.self_order_online_payment_method_id) + return super().create(vals_list) + + def write(self, vals): + # Because use_self_order_online_payment is not intended to be changed manually, + # avoid to raise an error. + if 'use_self_order_online_payment' not in vals: + return super().write(vals) + + can_change_self_order_domain = [('state', '=', 'draft')] + if vals['use_self_order_online_payment']: + can_change_self_order_domain = expression.AND([can_change_self_order_domain, [('config_id.self_order_online_payment_method_id', '!=', False)]]) + + can_change_self_order_orders = self.filtered_domain(can_change_self_order_domain) + cannot_change_self_order_orders = self - can_change_self_order_orders + + res = True + if can_change_self_order_orders: + res = super(PosOrder, can_change_self_order_orders).write(vals) and res + if cannot_change_self_order_orders: + clean_vals = vals.copy() + clean_vals.pop('use_self_order_online_payment', None) + res = super(PosOrder, cannot_change_self_order_orders).write(clean_vals) and res + + return res + + @api.depends('use_self_order_online_payment', 'config_id.self_order_online_payment_method_id', 'config_id.payment_method_ids') + def _compute_online_payment_method_id(self): + for order in self: + if order.use_self_order_online_payment: + # It is expected to use the self order online payment method. + # If for any reason it is not defined, then the online payment + # of the order is set to null to make the problem noticeable. + order.online_payment_method_id = order.config_id.self_order_online_payment_method_id + else: + super(PosOrder, order)._compute_online_payment_method_id() + + def get_and_set_online_payments_data(self, next_online_payment_amount=False): + res = super().get_and_set_online_payments_data(next_online_payment_amount) + if 'paid_order' not in res and not res.get('deleted', False) and not isinstance(next_online_payment_amount, bool): + # This method is only called in the POS frontend flow, not self order. + # If the next online payment is 0, then the online payment of the frontend + # flow is cancelled, and the default flow is self order if it is configured. + self.use_self_order_online_payment = tools.float_is_zero(next_online_payment_amount, precision_rounding=self.currency_id.rounding) and self.config_id.self_order_online_payment_method_id + return res diff --git a/addons/pos_online_payment_self_order/models/res_config_settings.py b/addons/pos_online_payment_self_order/models/res_config_settings.py new file mode 100644 index 00000000000..55ad58afa4d --- /dev/null +++ b/addons/pos_online_payment_self_order/models/res_config_settings.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + pos_self_order_online_payment_method_id = fields.Many2one(related='pos_config_id.self_order_online_payment_method_id', readonly=False) diff --git a/addons/pos_online_payment_self_order/static/src/pages/order_cart/order_cart.js b/addons/pos_online_payment_self_order/static/src/pages/order_cart/order_cart.js new file mode 100644 index 00000000000..0296b026359 --- /dev/null +++ b/addons/pos_online_payment_self_order/static/src/pages/order_cart/order_cart.js @@ -0,0 +1,71 @@ +/** @odoo-module */ +import { patch } from "@web/core/utils/patch"; +import { OrderCart } from "@pos_self_order/pages/order_cart/order_cart"; +import { _t } from "@web/core/l10n/translation"; + +patch(OrderCart.prototype, "pos_online_payment_self_order.OrderCart", { + get buttonToShow() { + if (this.selfOrder.self_order_mode === "each") { + return { label: _t("Pay"), disabled: false }; + } else if (this.selfOrder.self_order_mode === "meal") { + const order = this.selfOrder.currentOrder; + if (!order) { + return { label: "", disabled: true }; + } + if (!order.isSavedOnServer) { + return { label: _t("Order"), disabled: false }; + } else { + if (this.selfOrder.has_self_order_online_payment_method) { + return { label: _t("Pay"), disabled: false }; + } else { + return { label: _t("Pay at cashier"), disabled: true }; + } + } + } else { + return this._super(...arguments); + } + }, + async processOrder() { + if (this.sendInProgress) { + return; + } + + if (!this.selfOrder.table) { + this.state.selectTable = true; + return; + } + + if (this.selfOrder.self_order_mode === "meal" && this.selfOrder.has_self_order_online_payment_method) { + const order = this.selfOrder.currentOrder; + if (!order) { + this.selfOrder.notification.add(_t("The current order is invalid."), { type: "danger" }); + return; + } + if (!order.isSavedOnServer) { + this.sendInProgress = true; + await this.selfOrder.sendDraftOrderToServer(); + this.sendInProgress = false; + } else { + this.checkAndOpenPaymentPage(order); + } + } else if (this.selfOrder.self_order_mode === "each") { + this.sendInProgress = true; + const order = await this.selfOrder.sendDraftOrderToServer(); + this.sendInProgress = false; + this.checkAndOpenPaymentPage(order); + } else { + return this._super(...arguments); + } + }, + checkAndOpenPaymentPage(order) { + if (order) { + if (order.state === "draft") { + this.selfOrder.openOnlinePaymentPage(order); + } else { + this.selfOrder.notification.add(_t("The current order cannot be paid (maybe it is already paid)."), { type: "danger" }); + } + } else { + this.selfOrder.notification.add(_t("The order could not be saved, therefore its payment is unavailable."), { type: "danger" }); + } + }, +}); diff --git a/addons/pos_online_payment_self_order/static/src/self_order_service.js b/addons/pos_online_payment_self_order/static/src/self_order_service.js new file mode 100644 index 00000000000..c47f45b04a2 --- /dev/null +++ b/addons/pos_online_payment_self_order/static/src/self_order_service.js @@ -0,0 +1,17 @@ +/** @odoo-module */ +import { patch } from "@web/core/utils/patch"; +import { SelfOrder } from "@pos_self_order/self_order_service"; +import { session } from "@web/session"; + +patch(SelfOrder.prototype, "pos_online_payment_self_order.SelfOrder", { + openOnlinePaymentPage({ id: order_id, access_token: order_access_token, pos_config_id: order_pos_config_id }) { + const baseUrl = session.base_url; + let exitRouteUrl = baseUrl + "/menu/" + order_pos_config_id + "?access_token=" + this.access_token; + const tableIdentifier = this.table?.identifier; + if (tableIdentifier) { + exitRouteUrl += "&table_identifier=" + tableIdentifier; + } + const exitRoute = encodeURIComponent(exitRouteUrl); + window.open(baseUrl + `/pos/pay/${order_id}?access_token=${order_access_token}&exit_route=${exitRoute}`, "_self"); + }, +}); diff --git a/addons/pos_online_payment_self_order/static/tests/tours/self_order_after_each_cart_tour.js b/addons/pos_online_payment_self_order/static/tests/tours/self_order_after_each_cart_tour.js new file mode 100644 index 00000000000..952c7eb72ea --- /dev/null +++ b/addons/pos_online_payment_self_order/static/tests/tours/self_order_after_each_cart_tour.js @@ -0,0 +1,19 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { PosSelf } from "@pos_self_order/../tests/tours/tour_utils"; + +registry.category("web_tour.tours").add("self_order_after_each_cart_tour", { + test: true, + steps: () => [ + // Check that the self is open + PosSelf.check.isNotNotification(), + + PosSelf.action.clickPrimaryBtn("View Menu"), + ...PosSelf.action.addProduct("Office Chair Black", 1), + + PosSelf.action.clickPrimaryBtn("Review"), + PosSelf.check.isOrderline("Office Chair Black", "138.58", ""), + PosSelf.check.isPrimaryBtn("Pay"), // Not clicked on because it would open another page, losing the tour setup. + ], +}); diff --git a/addons/pos_online_payment_self_order/tests/__init__.py b/addons/pos_online_payment_self_order/tests/__init__.py new file mode 100644 index 00000000000..3796593f150 --- /dev/null +++ b/addons/pos_online_payment_self_order/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_self_order_frontend 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 new file mode 100644 index 00000000000..92803a3aaef --- /dev/null +++ b/addons/pos_online_payment_self_order/tests/test_self_order_frontend.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from unittest.mock import patch + +import odoo.tests +from odoo.addons.pos_self_order.tests.self_order_common_test import SelfOrderCommonTest +from odoo.addons.pos_online_payment.models.pos_payment_method import PosPaymentMethod + + +@odoo.tests.tagged("post_install", "-at_install") +class TestSelfOrderFrontendMobile(SelfOrderCommonTest): + + def test_self_order_pay_after_each_tour(self): + self.pos_config.self_order_table_mode = True + self.self_order_online_payment_method_id = self.env['pos.payment.method'].sudo()._get_or_create_online_payment_method(self.pos_config.company_id.id, self.pos_config.id) + self.fake_provider = self.env['payment.provider'].create({ + 'name': 'SelfOrderTest', + }) + real_get_online_payment_providers = PosPaymentMethod._get_online_payment_providers + def _fake_get_online_payment_providers(method_self, pos_config_id=False, error_if_invalid=True): + if method_self.id == self.self_order_online_payment_method_id.id: + return self.fake_provider + else: + return real_get_online_payment_providers(method_self, pos_config_id, error_if_invalid) + + with patch.object(PosPaymentMethod, '_get_online_payment_providers', _fake_get_online_payment_providers): + self.pos_config.update({ + 'self_order_pay_after': 'each', + 'self_order_online_payment_method_id': self.self_order_online_payment_method_id, + }) + self.pos_config.open_ui() + + self.start_tour( + self.pos_config._get_self_order_route(), + "self_order_after_each_cart_tour", + login=None, + ) + + self.fake_provider.unlink() diff --git a/addons/pos_online_payment_self_order/views/res_config_settings_views.xml b/addons/pos_online_payment_self_order/views/res_config_settings_views.xml new file mode 100644 index 00000000000..1796f79909a --- /dev/null +++ b/addons/pos_online_payment_self_order/views/res_config_settings_views.xml @@ -0,0 +1,17 @@ + + + + res.config.settings.view.form.inherit.pos_online_payment.view + res.config.settings + + + +
+
+
+
+
+
diff --git a/addons/pos_self_order/controllers/entry.py b/addons/pos_self_order/controllers/entry.py index d558f491a72..bba59fd5c5a 100644 --- a/addons/pos_self_order/controllers/entry.py +++ b/addons/pos_self_order/controllers/entry.py @@ -67,6 +67,7 @@ class PosQRMenuController(http.Controller): 'access_token': pos_config_access_token, **pos_config_sudo._get_self_order_data(), }, + "base_url": request.env['pos.session'].get_base_url(), } } ) diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py index 8d0fcc4a284..e9e1770ce98 100644 --- a/addons/pos_self_order/models/pos_config.py +++ b/addons/pos_self_order/models/pos_config.py @@ -34,7 +34,7 @@ class PosConfig(models.Model): help="Allow customers to Order from their phones", ) self_order_pay_after = fields.Selection( - [("each", "Each Order"), ("meal", "Meal")], + [("each", "Each Order (mobile payment only)"), ("meal", "Meal (mobile payment or cashier)")], string="Pay After:", default="meal", help="Choose when the customer will pay", diff --git a/addons/pos_self_order/models/res_config_settings.py b/addons/pos_self_order/models/res_config_settings.py index f0e4b1d918d..1b3933de7ac 100644 --- a/addons/pos_self_order/models/res_config_settings.py +++ b/addons/pos_self_order/models/res_config_settings.py @@ -14,7 +14,7 @@ class ResConfigSettings(models.TransientModel): compute="_compute_pos_module_pos_self_order", store=True, readonly=False ) pos_self_order_pay_after = fields.Selection( - [("each", "Each Order"), ("meal", "Meal")], + [("each", "Each Order (mobile payment only)"), ("meal", "Meal (mobile payment or cashier)")], string="Pay After:", default="meal", help="Choose when the customer will pay", @@ -40,7 +40,7 @@ class ResConfigSettings(models.TransientModel): { "pos_self_order_view_mode": False, "pos_self_order_table_mode": False, - "pos_self_order_pay_after": "each", + "pos_self_order_pay_after": "meal", "pos_self_order_image": False, "pos_self_order_image_name": False, } diff --git a/addons/pos_self_order/static/src/components/lines/lines.js b/addons/pos_self_order/static/src/components/lines/lines.js index 1faa18e030b..c2d296c5de8 100644 --- a/addons/pos_self_order/static/src/components/lines/lines.js +++ b/addons/pos_self_order/static/src/components/lines/lines.js @@ -20,10 +20,7 @@ export class Lines extends Component { clickOnLine(line) { const order = this.props.order; this.selfOrder.editedLine = line; - if ( - !order.access_token || - (order.state === "draft" && this.selfOrder.self_order_mode === "meal") - ) { + if (order.state === "draft") { this.selfOrder.editedOrder = order; this.router.navigate("product", { id: line.product_id }); } else { diff --git a/addons/pos_self_order/static/src/models/order.js b/addons/pos_self_order/static/src/models/order.js index 5dc3f29409d..b55f8a0b35b 100644 --- a/addons/pos_self_order/static/src/models/order.js +++ b/addons/pos_self_order/static/src/models/order.js @@ -67,7 +67,7 @@ export class Order extends Reactive { }, {}); } - isChange() { + hasNotAllLinesSent() { return this.lines.find((line) => { const lastSend = this.lastChangesSent[line.uuid]; @@ -78,4 +78,7 @@ export class Order extends Reactive { return lastSend.qty !== line.qty || line.isChange(lastSend); }); } + get isSavedOnServer() { + return this.isAlreadySent && !this.hasNotAllLinesSent(); + } } diff --git a/addons/pos_self_order/static/src/pages/order_cart/order_cart.js b/addons/pos_self_order/static/src/pages/order_cart/order_cart.js index 628dae9ad90..cd3661bd934 100644 --- a/addons/pos_self_order/static/src/pages/order_cart/order_cart.js +++ b/addons/pos_self_order/static/src/pages/order_cart/order_cart.js @@ -29,7 +29,10 @@ export class OrderCart extends Component { } get buttonToShow() { - return this.selfOrder.self_order_mode === "each" ? "Pay" : "Order"; + return { + label: this.selfOrder.self_order_mode === "each" ? _t("Pay") : _t("Order"), + disabled: false, + }; } get orderToPay() { diff --git a/addons/pos_self_order/static/src/pages/order_cart/order_cart.xml b/addons/pos_self_order/static/src/pages/order_cart/order_cart.xml index 0abbbf5abc1..b5535a3f6b1 100644 --- a/addons/pos_self_order/static/src/pages/order_cart/order_cart.xml +++ b/addons/pos_self_order/static/src/pages/order_cart/order_cart.xml @@ -13,7 +13,7 @@