[IMP] payment, website_payment: perform the donation payment

This commit adds /donation/* routes to perform the donation:
- /pay: shows the form with the contact details and donation details
- /transaction: creates the partner_id if missing and executes the
transaction
- /confirm: displays the payment success page

PR-63133

task-2398403

Part-of: odoo/odoo#63133
This commit is contained in:
Benoit Socias
2021-09-03 02:03:15 +00:00
committed by Jeremy Kersten
parent 5ffce116a6
commit 7fccbac004
15 changed files with 589 additions and 23 deletions
+25 -11
View File
@@ -142,7 +142,10 @@ class PaymentPortal(portal.CustomerPortal):
'invoice_id': invoice_id,
**self._get_custom_rendering_context_values(**kwargs),
}
return request.render('payment.pay', rendering_context)
return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context)
def _get_payment_page_template_xmlid(self, **kwargs):
return 'payment.pay'
@http.route('/my/payment_method', type='http', methods=['GET'], auth='user', website=True)
def payment_method(self, **kwargs):
@@ -205,16 +208,9 @@ class PaymentPortal(portal.CustomerPortal):
tx_sudo = self._create_transaction(
amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
)
# The generic landing route require the tx id and access token to be provided, since there
# is no document to rely on. The access token is recomputed in case we are dealing with a
# validation transaction (acquirer-specific amount and currency).
access_token = payment_utils.generate_access_token(
tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
)
tx_sudo.landing_route = f'{tx_sudo.landing_route}' \
f'?tx_id={tx_sudo.id}&access_token={access_token}'
if tx_sudo.operation == 'validation':
# The amount and currency have been chosen for the validation, recompute the access token
self._update_landing_route(tx_sudo)
return tx_sudo._get_processing_values()
def _create_transaction(
@@ -308,6 +304,24 @@ class PaymentPortal(portal.CustomerPortal):
return tx_sudo
@staticmethod
def _update_landing_route(tx_sudo):
""" Recompute the access token stored in the landing route of validation transactions.
The generic landing route require the tx id and access token to be provided, since there is
no document to rely on. The access token is recomputed in case we are dealing with a
validation transaction (acquirer-specific amount and currency).
:param recordset tx_sudo: The transaction whose landing routes to update, as a
`payment.transaction` record.
:return: None
"""
access_token = payment_utils.generate_access_token(
tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
)
tx_sudo.landing_route = f'{tx_sudo.landing_route}' \
f'?tx_id={tx_sudo.id}&access_token={access_token}'
@http.route('/payment/confirmation', type='http', methods=['GET'], auth='public', website=True)
def payment_confirm(self, tx_id, access_token, **kwargs):
""" Display the payment confirmation page with the appropriate status message to the user.
+2 -1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
from . import models
+6
View File
@@ -14,7 +14,9 @@ This is a bridge module that adds multi-website support for payment acquirers.
'portal',
],
'data': [
'data/donation_data.xml',
'views/payment_acquirer.xml',
'views/donation_templates.xml',
'views/snippets/snippets.xml',
'views/snippets/s_donation.xml',
],
@@ -23,6 +25,10 @@ This is a bridge module that adds multi-website support for payment acquirers.
'website.assets_wysiwyg': [
'website_payment/static/src/snippets/s_donation/options.js',
],
'web.assets_frontend': [
'website_payment/static/src/js/website_payment_donation.js',
'website_payment/static/src/js/website_payment_form.js',
],
},
'license': 'LGPL-3',
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import portal
@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http, _
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.tools.json import scriptsafe as json_safe
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.controllers import portal as payment_portal
class PaymentPortal(payment_portal.PaymentPortal):
@http.route('/donation/pay', type='http', methods=['GET', 'POST'], auth='public', website=True, sitemap=False)
def donation_pay(self, **kwargs):
""" Behaves like PaymentPortal.payment_pay but for donation
:param dict kwargs: As the parameters of in payment_pay, with the additional:
- str donation_options: The options settled in the donation snippet
- str donation_descriptions: The descriptions for all prefilled amounts
:return: The rendered donation form
:rtype: str
:raise: werkzeug.exceptions.NotFound if the access token is invalid
"""
kwargs['is_donation'] = True
kwargs['currency_id'] = int(kwargs.get('currency', request.env.company.currency_id.id))
kwargs['amount'] = float(kwargs.get('amount', 25))
kwargs['donation_options'] = kwargs.get('donation_options', json_safe.dumps(dict(customAmount="freeAmount")))
if request.env.user._is_public():
kwargs['partner_id'] = request.env.user.partner_id.id
kwargs['access_token'] = payment_utils.generate_access_token(kwargs['partner_id'], kwargs['amount'], kwargs['currency_id'])
return self.payment_pay(**kwargs)
@http.route('/donation/transaction/<minimum_amount>', type='json', auth='public', website=True, sitemap=False)
def donation_transaction(self, amount, currency_id, partner_id, access_token, minimum_amount=0, **kwargs):
if float(amount) < float(minimum_amount):
raise ValidationError(_('Donation amount must be at least %.2f.', float(minimum_amount)))
use_public_partner = request.env.user._is_public() or not partner_id
if use_public_partner:
details = kwargs['partner_details']
if not details.get('name'):
raise ValidationError(_('Name is required.'))
if not details.get('email'):
raise ValidationError(_('Email is required.'))
if not details.get('country_id'):
raise ValidationError(_('Country is required.'))
partner_id = request.website.user_id.partner_id.id
del kwargs['partner_details']
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
tx_sudo = self._create_transaction(
amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
)
tx_sudo.is_donation = True
if use_public_partner:
tx_sudo.update({
'partner_name': details['name'],
'partner_email': details['email'],
'partner_country_id': details['country_id'],
})
elif not tx_sudo.partner_country_id:
tx_sudo.partner_country_id = kwargs['partner_details']['country_id']
self._update_landing_route(tx_sudo)
# Send a notification to warn that a donation has been made
recipient_email = kwargs['donation_recipient_email']
comment = kwargs['donation_comment']
tx_sudo._send_donation_email(True, comment, recipient_email)
return tx_sudo._get_processing_values()
def _get_custom_rendering_context_values(self, donation_options=None, donation_descriptions=None, is_donation=False, **kwargs):
rendering_context = super()._get_custom_rendering_context_values(**kwargs)
if is_donation:
user_sudo = request.env.user
logged_in = not user_sudo._is_public()
# If the user is logged in, take their partner rather than the partner set in the params.
# This is something that we want, since security rules are based on the partner, and created
# tokens should not be assigned to the public user. This should have no impact on the
# transaction itself besides making reconciliation possibly more difficult (e.g. The
# transaction and invoice partners are different).
partner_sudo = user_sudo.partner_id
partner_details = {}
countries = request.env['res.country']
if logged_in:
partner_details = {
'name': partner_sudo.name,
'email': partner_sudo.email,
'country_id': partner_sudo.country_id.id,
}
countries = request.env['res.country'].sudo().search([])
descriptions = request.httprequest.form.getlist('donation_descriptions')
donation_options = json_safe.loads(donation_options) if donation_options else {}
donation_amounts = json_safe.loads(donation_options.get('donationAmounts', '[]'))
rendering_context.update({
'is_donation': True,
'partner': partner_sudo,
'transaction_route': '/donation/transaction/%s' % donation_options.get('minimumAmount', 0),
'partner_details': partner_details,
'error': {},
'countries': countries,
'donation_options': donation_options,
'donation_amounts': donation_amounts,
'donation_descriptions': descriptions,
})
return rendering_context
def _get_payment_page_template_xmlid(self, **kwargs):
if kwargs.get('is_donation'):
return 'website_payment.donation_pay'
return super()._get_payment_page_template_xmlid(**kwargs)
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_template_donation" model="mail.template">
<field name="name">Donation</field>
<field name="model_id" ref="payment.model_payment_transaction" />
<field name="lang">${object.partner_id.lang}</field>
</record>
</data>
<template id="website_payment.donation_mail_body" name="Donation mail">
<table border="0" cellpadding="0" style="background-color: white; padding: 0px; border-collapse:separate;">
<tr style="height: 48px;"><td valign="top"><span style="font-size: 24px; font-weight: bold;">
<t t-if="is_internal_notification">Donation notification</t>
<t t-else="">Donation</t>
</span></td></tr>
<t t-if="not is_internal_notification">
<tr><td valign="top">
Dear <t t-out="tx.partner_name"/>,
</td></tr>
<tr><td valign="top">
<div style="margin: 16px 0px 16px 0px;">
Thank you for your donation of <span t-out="tx.amount" t-options="{'widget': 'monetary', 'display_currency': tx.currency_id}"/> made on <t t-out="tx.create_date" t-options="{'widget': 'date'}"/>.
<br/>
We appreciate your support for our organization as such.
<br/>
Regards.
</div>
</td></tr>
</t>
<tr><td valign="top">
<div style="margin: 16px 0px 16px 0px;">
<table border="0" cellpadding="0" cellspacing="5" width="100%">
<tr>
<td><b>Donor Name:</b></td>
<td><t t-out="tx.partner_name"/></td>
</tr>
<tr>
<td><b>Donor Email:</b></td>
<td><t t-out="tx.partner_email"/></td>
</tr>
<tr>
<td><b>Donation Date:</b></td>
<td><t t-out="tx.create_date.date()"/></td>
</tr>
<tr>
<td><b>Amount(<t t-out="tx.currency_id.symbol"/>):</b></td>
<td><t t-out="tx.amount"/></td>
</tr>
<tr t-if="is_internal_notification and comment">
<td><b>Comment:</b></td>
<td><t t-out="comment"/></td>
</tr>
<tr>
<td><b>Payment Method:</b></td>
<td><t t-out="tx.provider"/></td>
</tr>
<tr>
<td><b>Payment ID:</b></td>
<td><t t-out="tx.reference"/></td>
</tr>
</table>
</div>
</td></tr>
</table>
</template>
</odoo>
+6 -1
View File
@@ -1 +1,6 @@
from . import payment_acquirer
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment
from . import payment_acquirer
from . import payment_transaction
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountPayment(models.Model):
_inherit = 'account.payment'
is_donation = fields.Boolean(string="Is Donation", related="payment_transaction_id.is_donation", help="Is the payment a donation")
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, fields, models
class PaymentTransaction(models.Model):
_inherit = "payment.transaction"
is_donation = fields.Boolean(string="Is donation", help="Is the payment a donation")
def _finalize_post_processing(self):
super()._finalize_post_processing()
for tx in self.filtered('is_donation'):
tx._send_donation_email()
msg = [_('Payment received from donation with following details:')]
for field in ['company_id', 'partner_id', 'partner_name', 'partner_country_id', 'partner_email']:
field_name = tx._fields[field].string
value = tx[field]
if value:
if 'name' in value:
value = value.name
msg.append('<br/>- %s: %s' % (field_name, value))
tx.payment_id._message_log(body=''.join(msg))
def _send_donation_email(self, is_internal_notification=False, comment=None, recipient_email=None):
self.ensure_one()
if is_internal_notification or self.state == 'done':
subject = _('A donation has been made on your website') if is_internal_notification else _('Donation confirmation')
body = self.env.ref('website_payment.donation_mail_body')._render({
'is_internal_notification': is_internal_notification,
'tx': self,
'comment': comment,
}, engine='ir.qweb', minimal_qcontext=True)
self.env.ref('website_payment.mail_template_donation').send_mail(
self.id, notif_layout="mail.mail_notification_light",
force_send=True,
email_values={
'email_to': recipient_email if is_internal_notification else self.partner_email,
'email_from': self.company_id.email_formatted,
'author_id': self.partner_id.id,
'subject': subject,
'body_html': body,
},
)
@@ -0,0 +1,35 @@
/** @odoo-module **/
import publicWidget from 'web.public.widget';
publicWidget.registry.WebsitePaymentDonation = publicWidget.Widget.extend({
selector: '.o_donation_payment_form',
events: {
'focus .o_amount_input': '_onFocusAmountInput',
'change #donation_comment_checkbox': '_onChangeDonationComment'
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Event} ev
*/
_onFocusAmountInput(ev) {
this.$target.find('#other_amount').prop("checked", true);
},
/**
* @private
* @param {Event} ev
*/
_onChangeDonationComment(ev) {
const $donationComment = this.$target.find('#donation_comment');
const checked = $(ev.currentTarget).is(':checked');
$donationComment.toggleClass('d-none', !checked);
if (!checked) {
$donationComment.val('');
}
},
});
@@ -0,0 +1,78 @@
/** @odoo-module **/
import {_t} from 'web.core';
import checkoutForm from 'payment.checkout_form';
checkoutForm.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Perform some validations for donations before performing payment
*
* @override method from payment.payment_form_mixin
* @private
* @param {string} provider - The provider of the payment option's acquirer
* @param {number} paymentOptionId - The id of the payment option handling the transaction
* @param {string} flow - The online payment flow of the transaction
* @return {Promise}
*/
_processPayment: function (provider, paymentOptionId, flow) {
if ($('.o_donation_payment_form').length) {
const errorFields = {};
if (!this.$('input[name="email"]')[0].checkValidity()) {
errorFields['email'] = _t("Email is invalid");
}
const mandatoryFields = {
'name': _t('Name'),
'email': _t('Email'),
'country_id': _t('Country'),
};
for (const id in mandatoryFields) {
const $field = this.$('input[name="' + id + '"],select[name="' + id + '"]');
$field.removeClass('is-invalid').popover('dispose');
if (!$field.val().trim()) {
errorFields[id] = _.str.sprintf(_t("Field '%s' is mandatory"), mandatoryFields[id]);
}
}
if (Object.keys(errorFields).length) {
for (const id in errorFields) {
const $field = this.$('input[name="' + id + '"],select[name="' + id + '"]');
$field.addClass('is-invalid');
$field.popover({content: errorFields[id], trigger: 'hover', container: 'body', placement: 'top'});
$field.data("bs.popover").config.content = errorFields[id];
}
this._displayError(
_t("Validation Error"),
_t("Some information is missing to process your payment.")
);
return Promise.resolve();
}
}
return this._super(...arguments);
},
/**
* Add params used by the donation snippet to the transaction route params.
*
* @override method from payment.payment_form_mixin
* @private
* @param {string} provider - The provider of the selected payment option's acquirer
* @param {number} paymentOptionId - The id of the selected payment option
* @param {string} flow - The online payment flow of the selected payment option
* @return {object} The extended transaction route params
*/
_prepareTransactionRouteParams: function (provider, paymentOptionId, flow) {
const transactionRouteParams = this._super(...arguments);
return $('.o_donation_payment_form').length ? {
...transactionRouteParams,
'partner_details': {
'name': this.$('input[name="name"]').val(),
'email': this.$('input[name="email"]').val(),
'country_id': this.$('select[name="country_id"]').val(),
},
'donation_comment': this.$('#donation_comment').val(),
'donation_recipient_email': this.$('input[name="donation_recipient_email"]').val(),
} : transactionRouteParams;
},
});
@@ -8,6 +8,7 @@ publicWidget.registry.DonationSnippet = publicWidget.Widget.extend({
disabledInEditableMode: false,
events: {
'click .s_donation_btn': '_onClickPrefilledButton',
'click .s_donation_donate_btn': '_onClickDonateNowButton',
'input #s_donation_range_slider': '_onInputRangeSlider',
},
@@ -17,8 +18,9 @@ publicWidget.registry.DonationSnippet = publicWidget.Widget.extend({
async start() {
await this._super(...arguments);
this.$rangeSlider = this.$('#s_donation_range_slider');
this.defaultAmount = this.$target[0].dataset.defaultAmount;
if (this.$rangeSlider.length) {
this.$rangeSlider.val(this.$target[0].dataset.defaultAmount);
this.$rangeSlider.val(this.defaultAmount);
this._setBubble(this.$rangeSlider);
}
await this._displayCurrencies();
@@ -29,6 +31,7 @@ publicWidget.registry.DonationSnippet = publicWidget.Widget.extend({
destroy() {
this.$target.find('.s_donation_currency').remove();
this._deselectPrefilledButtons();
this.$('.alert-danger').remove();
this._super(...arguments);
},
@@ -100,6 +103,50 @@ publicWidget.registry.DonationSnippet = publicWidget.Widget.extend({
this._setBubble(this.$rangeSlider);
}
},
/**
* @private
*/
_onClickDonateNowButton(ev) {
if (this.editableMode) {
return;
};
this.$('.alert-danger').remove();
const $buttons = this.$('.s_donation_btn');
const $selectedButton = $buttons.filter('.active');
let amount = $selectedButton.length ? $selectedButton[0].dataset.donationValue : 0;
if (this.$target[0].dataset.displayOptions && !amount) {
if (this.$rangeSlider.length) {
amount = this.$rangeSlider.val();
} else if ($buttons.length) {
amount = parseFloat(this.$('#s_donation_amount_input').val());
let errorMessage = '';
const minAmount = this.$target[0].dataset.minimumAmount;
if (!amount) {
errorMessage = _t("Please select or enter an amount");
} else if (amount < parseFloat(minAmount)) {
const before = this.currency.position === "before" ? this.currency.symbol : "";
const after = this.currency.position === "after" ? this.currency.symbol : "";
errorMessage = _.str.sprintf(_t("The minimum donation amount is %s%s%s"), before, minAmount, after);
}
if (errorMessage) {
$(ev.currentTarget).before($('<p>', {
class: 'alert alert-danger',
text: errorMessage,
}));
return;
}
}
}
if (!amount) {
amount = this.defaultAmount;
}
const $form = this.$('.s_donation_form');
$('<input>').attr({type: 'hidden', name: 'amount', value: amount}).appendTo($form);
$('<input>').attr({type: 'hidden', name: 'currency_id', value: this.currency.id}).appendTo($form);
$('<input>').attr({type: 'hidden', name: 'csrf_token', value: odoo.csrf_token}).appendTo($form);
$('<input>').attr({type: 'hidden', name: 'donation_options', value: JSON.stringify(this.el.dataset)}).appendTo($form);
$form.submit();
},
/**
* @private
*/
@@ -4,11 +4,12 @@
<div class="s_donation_prefilled_buttons mb-2">
<t t-foreach="prefilled_buttons" t-as="prefilled_button_value">
<button class="s_donation_btn btn btn-outline-primary btn-lg mb-2 mr-1 o_not_editable"
type="button"
contenteditable="false"
t-att-data-donation-value="prefilled_button_value"
t-esc="prefilled_button_value"/>
</t>
<button t-if="custom_input" class="s_donation_btn s_donation_custom_btn btn btn-outline-primary btn-lg mb-2 mr-1">
<button t-if="custom_input" type="button" class="s_donation_btn s_donation_custom_btn btn btn-outline-primary btn-lg mb-2 mr-1">
<input id="s_donation_amount_input" type="number" min="5" class="" placeholder="Custom Amount" aria-label="Amount"/>
</button>
</div>
@@ -18,13 +19,14 @@
<t t-foreach="prefilled_buttons" t-as="prefilled_button">
<div class="s_donation_btn_description d-sm-flex align-items-center my-3 o_not_editable o_translate_mode_hidden" contenteditable="false">
<button class="s_donation_btn btn btn-outline-primary btn-lg mr-3"
type="button"
t-att-data-donation-value="prefilled_button.value"
t-esc="prefilled_button.value"/>
<p class="s_donation_description mt-2 my-sm-auto text-muted font-italic" t-esc="prefilled_button.description"></p>
</div>
</t>
<div t-if="custom_input" class="d-sm-flex align-items-center my-3">
<button class="s_donation_btn s_donation_custom_btn btn btn-outline-primary btn-lg">
<button type="button" class="s_donation_btn s_donation_custom_btn btn btn-outline-primary btn-lg">
<input id="s_donation_amount_input" type="number" t-att-min="minimum_amount" placeholder="Custom Amount" aria-label="Amount"/>
</button>
</div>
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="payment_checkout" inherit_id="payment.checkout">
<!-- Make available anchor to inject donation form content -->
<xpath expr="//form/t[@t-set='acquirer_count']" position="before">
<t t-if="is_donation">
<t t-set="donation_confirm_button_icon_class" t-valuef="fa-gift"/>
<t t-set="donation_confirm_button_label">Donate Now</t>
<h3 class="o_page_header mt16 mb4">Donation</h3>
<div class="form-row">
<div t-attf-class="form-group #{error.get('name') and 'o_has_error' or ''} col-lg-12 div_name">
<label class="col-form-label font-weight-bold" for="name">Name
<span class="s_website_form_mark"> *</span>
</label>
<input t-att-readonly="'1' if 'name' in partner_details and partner_id else None" type="text" name="name" t-attf-class="form-control #{error.get('name') and 'is-invalid' or ''}" t-att-value="partner_details.get('name')" />
</div>
<div class="w-100"/>
<div t-attf-class="form-group #{error.get('email') and 'o_has_error' or ''} col-lg-6" id="div_email">
<label class="col-form-label font-weight-bold" for="email">Email
<span class="s_website_form_mark"> *</span>
</label>
<input t-att-readonly="'1' if 'email' in partner_details and partner_id else None" type="email" name="email" t-attf-class="form-control #{error.get('email') and 'is-invalid' or ''}" t-att-value="partner_details.get('email')" />
</div>
<t t-set="country" t-value="partner_details.get('country')"/>
<div t-attf-class="form-group #{error.get('country_id') and 'o_has_error' or ''} col-lg-6 div_country">
<label class="col-form-label font-weight-bold" for="country_id">Country
<span class="s_website_form_mark"> *</span>
</label>
<select t-att-disabled="'1' if country and country.id and partner_id else None" id="country_id" name="country_id" t-attf-class="form-control #{error.get('country_id') and 'is-invalid' or ''}">
<option value="">Country...</option>
<t t-foreach="countries" t-as="c">
<option t-att-value="c.id" t-att-selected="c.id == (country and country.id or -1)">
<t t-out="c.name" />
</option>
</t>
</select>
</div>
<div class="w-100"/>
<div class="form-group col-lg-12 o_donation_payment_form">
<div class="col-lg-6 px-0">
<label class="col-form-label font-weight-bold">Amount (<t t-out="currency.symbol"/>)</label>
<t t-set="donation_layout" t-value="donation_options.get('customAmount')"/>
<t t-set="prefilled_options" t-value="donation_options.get('prefilledOptions')"/>
<t t-if="prefilled_options">
<t t-foreach="donation_amounts" t-as="donation_amount">
<div class="custom-control custom-radio my-2">
<t t-set="is_checked" t-value="float(amount) == float(donation_amount)"/>
<t t-set="has_checked" t-value="has_checked or is_checked"/>
<input class="custom-control-input" type="radio" name="amount"
t-attf-id="amount_#{donation_amount_index}" t-att-value="donation_amount"
t-att-checked="is_checked or None"/>
<label class="custom-control-label mt-0" t-attf-for="amount_#{donation_amount_index}">
<t t-out="donation_amount"/>
<span t-if="donation_options.get('descriptions')" class="text-muted font-italic ml-1">
- <t t-out="donation_descriptions[donation_amount_index]"/>
</span>
</label>
</div>
</t>
<div t-attf-class="custom-control custom-radio my-2 #{not donation_layout and 'd-none' or ''}">
<input class="custom-control-input" type="radio" id="other_amount" name="amount"
t-att-value="amount" t-att-checked="not has_checked or None"/>
<label class="custom-control-label mt-0 d-block" for="other_amount">
<t t-call="website_payment.donation_input">
<t t-set="amount" t-value="not has_checked and amount or ''"/>
</t>
</label>
</div>
</t>
<t t-else="">
<t t-call="website_payment.donation_input"/>
</t>
</div>
<div class="col-lg-12 px-0">
<div class="custom-control custom-checkbox mt-3">
<input class="custom-control-input" type="checkbox" value="" id="donation_comment_checkbox"/>
<label class="custom-control-label" for="donation_comment_checkbox">Write us a comment</label>
</div>
<textarea class="form-control d-none mt-2" id="donation_comment" placeholder="Your comment"/>
</div>
</div>
<input type="hidden" name="donation_recipient_email" t-att-value="donation_options.get('donationEmail')"/>
<div class="w-100"/>
</div>
<h3 class="o_page_header mt16 mb4">Payment Details</h3>
</t>
</xpath>
<!-- Adapt Pay confirm button -->
<xpath expr="//t[@t-set='label']" position="after">
<t t-set="label" t-value="donation_confirm_button_label or label"/>
</xpath>
<xpath expr="//t[@t-set='icon_class']" position="after">
<t t-set="icon_class" t-value="donation_confirm_button_icon_class or icon_class"/>
</xpath>
</template>
<!-- Display of /donation/pay -->
<template id="website_payment.donation_pay" name="Donation payment">
<t t-call="portal.frontend_layout">
<t t-set="page_title" t-value="'Donation'"/>
<t t-set="additional_title"><t t-out="page_title"/></t>
<div class="wrap">
<div class="oe_structure" id="oe_structure_website_payment_donation_1"/>
<div class="container mb-3">
<!-- Portal breadcrumb -->
<t t-call="payment.portal_breadcrumb"/>
<!-- Payment page -->
<div class="row">
<div class="col-lg-7">
<div t-if="not amount" class="alert alert-info">
There is nothing to pay.
</div>
<div t-elif="not currency" class="alert alert-warning">
<strong>Warning</strong> The currency is missing or incorrect.
</div>
<div t-elif="not acquirers and not tokens" class="alert alert-warning">
<strong>No suitable payment option could be found.</strong><br/>
If you believe that it is an error, please contact the website administrator.
</div>
<t t-else="" t-call="payment.checkout"/>
</div>
</div>
</div>
<div class="oe_structure" id="oe_structure_website_payment_donation_2"/>
</div>
</t>
</template>
<template id="website_payment.donation_input" name="Donation input">
<input type="number" class="form-control o_amount_input" t-att-min="donation_options.get('minimumAmount')"
t-att-max="donation_options.get('maximumAmount')" t-att-value="amount" placeholder="Custom Amount"/>
</template>
</odoo>
@@ -14,13 +14,15 @@
data-maximum-amount="100"
data-slider-step="5"
data-default-amount="25">
<span id="s_donation_description_inputs">
<input type="hidden" class="o_translatable_input_hidden d-block mb-1 w-100" name="donation_descriptions[]" value="A year of cultural awakening."/>
<input type="hidden" class="o_translatable_input_hidden d-block mb-1 w-100" name="donation_descriptions[]" value="Caring for a baby for 1 month."/>
<input type="hidden" class="o_translatable_input_hidden d-block mb-1 w-100" name="donation_descriptions[]" value="One year in elementary school."/>
<input type="hidden" class="o_translatable_input_hidden d-block mb-1 w-100" name="donation_descriptions[]" value="One year in high school."/>
</span>
<a href="#" class="s_donation_donate_btn btn btn-secondary btn-lg mb-2">Donate Now</a>
<form class="s_donation_form" action="/donation/pay" method="post" enctype="multipart/form-data">
<span id="s_donation_description_inputs">
<input type="hidden" class="o_translatable_input_hidden d-block mb-1 w-100" name="donation_descriptions" value="A year of cultural awakening."/>
<input type="hidden" class="o_translatable_input_hidden d-block mb-1 w-100" name="donation_descriptions" value="Caring for a baby for 1 month."/>
<input type="hidden" class="o_translatable_input_hidden d-block mb-1 w-100" name="donation_descriptions" value="One year in elementary school."/>
<input type="hidden" class="o_translatable_input_hidden d-block mb-1 w-100" name="donation_descriptions" value="One year in high school."/>
</span>
<a href="#" type="button" class="s_donation_donate_btn btn btn-secondary btn-lg mb-2">Donate Now</a>
</form>
</div>
</template>