diff --git a/addons/payment/__manifest__.py b/addons/payment/__manifest__.py index 4b7e5b17484..42108fa8045 100644 --- a/addons/payment/__manifest__.py +++ b/addons/payment/__manifest__.py @@ -40,6 +40,7 @@ 'payment/static/src/scss/payment_form.scss', 'payment/static/lib/jquery.payment/jquery.payment.js', 'payment/static/src/js/checkout_form.js', + 'payment/static/src/js/express_checkout_form.js', 'payment/static/src/js/manage_form.js', 'payment/static/src/js/payment_form_mixin.js', 'payment/static/src/js/post_processing.js', diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 22d1118ebf2..f3322882b5d 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -55,6 +55,12 @@ class PaymentAcquirer(models.Model): help="Capture the amount from Odoo, when the delivery is completed.\n" "Use this if you want to charge your customers cards only when\n" "you are sure you can ship the goods to them.") + allow_express_checkout = fields.Boolean( + string="Allow Express Checkout", + help="This controls whether customers can use express payment methods. Express checkout " + "enables customers to pay with Google Pay and Apple Pay from which address " + "information is collected at payment.", + ) redirect_form_view_id = fields.Many2one( string="Redirect Form Template", comodel_name='ir.ui.view', help="The template rendering a form submitted to redirect the user when making a payment", @@ -69,6 +75,12 @@ class PaymentAcquirer(models.Model): help="The template rendering the inline payment form when making a payment by token.", domain=[('type', '=', 'qweb')], ) + express_checkout_form_view_id = fields.Many2one( + string="Express Checkout Form Template", + comodel_name='ir.ui.view', + help="The template rendering the express payment methods' form.", + domain=[('type', '=', 'qweb')], + ) journal_id = fields.Many2one( string="Payment Journal", comodel_name='account.journal', compute='_compute_journal_id', inverse='_inverse_journal_id', @@ -126,19 +138,22 @@ class PaymentAcquirer(models.Model): default=lambda self: _("Your payment has been cancelled."), translate=True) # Feature support fields - support_fees = fields.Boolean( - string="Fees Supported", compute='_compute_feature_support_fields' + support_tokenization = fields.Boolean( + string="Tokenization Supported", compute='_compute_feature_support_fields' ) support_manual_capture = fields.Boolean( string="Manual Capture Supported", compute='_compute_feature_support_fields' ) + support_express_checkout = fields.Boolean( + string="Express Checkout Supported", compute='_compute_feature_support_fields' + ) support_refund = fields.Selection( string="Type of Refund Supported", selection=[('full_only', "Full Only"), ('partial', "Partial")], compute='_compute_feature_support_fields', ) - support_tokenization = fields.Boolean( - string="Tokenization Supported", compute='_compute_feature_support_fields' + support_fees = fields.Boolean( + string="Fees Supported", compute='_compute_feature_support_fields' ) # Kanban view fields @@ -158,6 +173,7 @@ class PaymentAcquirer(models.Model): # View configuration fields show_credentials_page = fields.Boolean(compute='_compute_view_configuration_fields') show_allow_tokenization = fields.Boolean(compute='_compute_view_configuration_fields') + show_allow_express_checkout = fields.Boolean(compute='_compute_view_configuration_fields') show_payment_icon_ids = fields.Boolean(compute='_compute_view_configuration_fields') show_pre_msg = fields.Boolean(compute='_compute_view_configuration_fields') show_pending_msg = fields.Boolean(compute='_compute_view_configuration_fields') @@ -196,6 +212,7 @@ class PaymentAcquirer(models.Model): self.update({ 'show_credentials_page': True, 'show_allow_tokenization': True, + 'show_allow_express_checkout': True, 'show_payment_icon_ids': True, 'show_pre_msg': True, 'show_pending_msg': True, @@ -245,10 +262,13 @@ class PaymentAcquirer(models.Model): :return: None """ - self.update(dict.fromkeys( - ('support_fees', 'support_manual_capture', 'support_refund', 'support_tokenization'), - None, - )) + self.update(dict.fromkeys(( + 'support_express_checkout', + 'support_fees', + 'support_manual_capture', + 'support_refund', + 'support_tokenization', + ), None)) def _get_default_payment_method_id(self): self.ensure_one() @@ -393,7 +413,7 @@ class PaymentAcquirer(models.Model): @api.model def _get_compatible_acquirers( self, company_id, partner_id, amount, currency_id=None, force_tokenization=False, - is_validation=False, **kwargs + is_express_checkout=False, is_validation=False, **kwargs ): """ Select and return the acquirers matching the criteria. @@ -405,6 +425,7 @@ class PaymentAcquirer(models.Model): :param float amount: The amount to pay, `0` for validation transactions. :param int currency_id: The payment currency if known beforehand, as a `res.currency` id :param bool force_tokenization: Whether only acquirers allowing tokenization can be matched + :param bool is_express_checkout: Whether the payment is made through express checkout. :param bool is_validation: Whether the operation is a validation :param dict kwargs: Optional data. This parameter is not used here :return: The compatible acquirers @@ -447,6 +468,10 @@ class PaymentAcquirer(models.Model): if force_tokenization or self._is_tokenization_required(**kwargs): domain = expression.AND([domain, [('allow_tokenization', '=', True)]]) + # Handle express checkout. + if is_express_checkout: + domain = expression.AND([domain, [('allow_express_checkout', '=', True)]]) + compatible_acquirers = self.env['payment.acquirer'].search(domain) return compatible_acquirers diff --git a/addons/payment/models/payment_transaction.py b/addons/payment/models/payment_transaction.py index 45d53510e22..e5b08fcc5a0 100644 --- a/addons/payment/models/payment_transaction.py +++ b/addons/payment/models/payment_transaction.py @@ -485,6 +485,7 @@ class PaymentTransaction(models.Model): self.ensure_one() processing_values = { + 'tx_id': self.id, 'acquirer_id': self.acquirer_id.id, 'provider': self.provider, 'reference': self.reference, diff --git a/addons/payment/static/src/js/express_checkout_form.js b/addons/payment/static/src/js/express_checkout_form.js new file mode 100644 index 00000000000..5ae73d4a8a9 --- /dev/null +++ b/addons/payment/static/src/js/express_checkout_form.js @@ -0,0 +1,122 @@ +/** @odoo-module */ + +import core from 'web.core'; +import publicWidget from 'web.public.widget'; + +publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({ + selector: 'form[name="o_payment_express_checkout_form"]', + + /** + * @override + */ + start: async function () { + await this._super(...arguments); + this.txContext = {}; + Object.assign(this.txContext, this.$el.data()); + const expressCheckoutForms = this._getExpressCheckoutForms(); + for (const expressCheckoutForm of expressCheckoutForms) { + await this._prepareExpressCheckoutForm(expressCheckoutForm.dataset); + } + // Monitor updates of the amount on eCommerce's cart pages. + core.bus.on('cart_amount_changed', this, this._updateAllAmounts.bind(this)); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Return all express checkout forms found on the page. + * + * @private + * @return {NodeList} - All express checkout forms found on the page. + */ + _getExpressCheckoutForms() { + return document.querySelectorAll( + 'form[name="o_payment_express_checkout_form"] div[name="o_express_checkout_container"]' + ); + }, + + /** + * Return whether the shipping information is required or not. + * + * For a module to request shipping information to the customer, it must override this method. + * + * @private + * @return {Boolean} - Whether the shipping information is required or not. + */ + _isShippingInformationRequired: () => false, + + /** + * Prepare the acquirer-specific express checkout form based on the provided data. + * + * For an acquirer to manage an express checkout form, it must override this method. + * + * @private + * @param {Object} acquirerData - The acquirer-specific data. + * @return {Promise} + */ + async _prepareExpressCheckoutForm(acquirerData) { + return Promise.resolve(); + }, + + /** + * Prepare the params to send to the transaction route. + * + * For an acquirer to overwrite generic params or to add acquirer-specific ones, it must + * override this method and return the extended transaction route params. + * + * @private + * @param {number} acquirerId - The id of the acquirer handling the transaction. + * @returns {object} - The transaction route params + */ + _prepareTransactionRouteParams(acquirerId) { + return { + 'payment_option_id': parseInt(acquirerId), + 'reference_prefix': this.txContext.referencePrefix && + this.txContent.referencePrefix.toString(), + 'currency_id': this.txContext.currencyId && + parseInt(this.txContext.currencyId), + 'partner_id': parseInt(this.txContext.partnerId), + 'flow': 'direct', + 'tokenization_requested': false, + 'landing_route': this.txContext.landingRoute, + 'add_id_to_landing_route': true, + 'access_token': this.txContext.accessToken, + 'csrf_token': core.csrf_token, + }; + }, + + /** + * Retrieve all the express checkout forms and update the amount in each form. + * + * Note: triggered by bus event `cart_amount_changed`. + * + * @private + * @param {number} newAmount - The new amount. + * @param {number} newMinorAmount - The new minor amount. + * @return {undefined} + */ + _updateAllAmounts(newAmount, newMinorAmount) { + const expressCheckoutForms = this._getExpressCheckoutForms(); + for (const expressCheckoutForm of expressCheckoutForms) { + this._updateAmount(expressCheckoutForm.dataset, newAmount, newMinorAmount); + } + }, + + /** + * Update the amount of the express checkout form. + * + * For an acquirer to manage an express form, it must override this method. + * + * @private + * @param {Object} acquirerData - The acquirer-specific data. + * @param {number} newAmount - The new amount. + * @param {number} newMinorAmount - The new minor amount. + * @return {undefined} + */ + _updateAmount(acquirerData, newAmount, newMinorAmount) {}, + +}); + +export const paymentExpressCheckoutForm = publicWidget.registry.PaymentExpressCheckoutForm; diff --git a/addons/payment/views/payment_acquirer_views.xml b/addons/payment/views/payment_acquirer_views.xml index 0e286290bbe..a97912eb382 100644 --- a/addons/payment/views/payment_acquirer_views.xml +++ b/addons/payment/views/payment_acquirer_views.xml @@ -12,10 +12,12 @@ + + @@ -83,6 +85,7 @@ + diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml index f9eb75c487e..1e6478a1e52 100644 --- a/addons/payment/views/payment_templates.xml +++ b/addons/payment/views/payment_templates.xml @@ -292,6 +292,49 @@ + + +