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 @@
+
+