[IMP] payment(_demo): support express checkout
This will allow users to test the "express checkout" feature in eCommerce without requiring to setup a real payment provider. task-3047772 closes odoo/odoo#113644 Signed-off-by: Valentin Chevalier <vcr@odoo.com>
This commit is contained in:
committed by
Valentin Chevalier
parent
a1d670ca30
commit
c375b8f534
@@ -36,7 +36,7 @@
|
||||
t-att-data-express-checkout-route="express_checkout_route"
|
||||
t-att-data-landing-route="landing_route"
|
||||
>
|
||||
<t t-foreach="providers" t-as="provider_sudo">
|
||||
<t t-foreach="providers_sudo" t-as="provider_sudo">
|
||||
<t t-set="express_checkout_form_xml_id"
|
||||
t-value="provider_sudo.express_checkout_form_view_id.xml_id"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<field name="code">demo</field>
|
||||
<field name="inline_form_view_id" ref="inline_form"/>
|
||||
<field name="token_inline_form_view_id" ref="token_inline_form"/>
|
||||
<field name="express_checkout_form_view_id" ref="express_checkout_form"/>
|
||||
<field name="allow_tokenization">True</field>
|
||||
<field name="allow_express_checkout">True</field>
|
||||
<field name="payment_method_ids"
|
||||
eval="[Command.set([
|
||||
ref('payment_demo.payment_method_demo'),
|
||||
|
||||
@@ -24,6 +24,7 @@ class PaymentProvider(models.Model):
|
||||
""" Override of `payment` to enable additional features. """
|
||||
super()._compute_feature_support_fields()
|
||||
self.filtered(lambda p: p.code == 'demo').update({
|
||||
'support_express_checkout': True,
|
||||
'support_manual_capture': 'partial',
|
||||
'support_refund': 'partial',
|
||||
'support_tokenization': True,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import publicWidget from '@web/legacy/js/public/public_widget';
|
||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||
import { debounce } from '@web/core/utils/timing';
|
||||
|
||||
import { paymentExpressCheckoutForm } from '@payment/js/express_checkout_form';
|
||||
import paymentDemoMixin from '@payment_demo/js/payment_demo_mixin';
|
||||
|
||||
paymentExpressCheckoutForm.include({
|
||||
events: Object.assign({}, publicWidget.Widget.prototype.events, {
|
||||
'click button[name="o_payment_submit_button"]': '_initiateExpressPayment',
|
||||
}),
|
||||
|
||||
// #=== WIDGET LIFECYCLE ===#
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: async function () {
|
||||
await this._super(...arguments);
|
||||
document.querySelector('[name="o_payment_submit_button"]')?.removeAttribute('disabled');
|
||||
this._initiateExpressPayment = debounce(this._initiateExpressPayment, 500, true);
|
||||
},
|
||||
|
||||
// #=== EVENT HANDLERS ===#
|
||||
|
||||
/**
|
||||
* Process the payment.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {void}
|
||||
*/
|
||||
async _initiateExpressPayment(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const shippingInformationRequired = document.querySelector(
|
||||
'[name="o_payment_express_checkout_form"]'
|
||||
).dataset.shippingInfoRequired;
|
||||
const providerId = ev.target.parentElement.dataset.providerId;
|
||||
let expressShippingAddress = {};
|
||||
if (shippingInformationRequired){
|
||||
const shippingInfo = document.querySelector(
|
||||
`#o_payment_demo_shipping_info_${providerId}`
|
||||
);
|
||||
expressShippingAddress = {
|
||||
'name': shippingInfo.querySelector('#o_payment_demo_shipping_name').value,
|
||||
'email': shippingInfo.querySelector('#o_payment_demo_shipping_email').value,
|
||||
'street': shippingInfo.querySelector('#o_payment_demo_shipping_address').value,
|
||||
'street2': shippingInfo.querySelector('#o_payment_demo_shipping_address2').value,
|
||||
'country': shippingInfo.querySelector('#o_payment_demo_shipping_zip').value,
|
||||
'city': shippingInfo.querySelector('#o_payment_demo_shipping_city').value,
|
||||
'zip':shippingInfo.querySelector('#o_payment_demo_shipping_country').value
|
||||
};
|
||||
}
|
||||
await jsonrpc(
|
||||
document.querySelector(
|
||||
'[name="o_payment_express_checkout_form"]'
|
||||
).dataset['expressCheckoutRoute'],
|
||||
{
|
||||
'shipping_address': expressShippingAddress,
|
||||
'billing_address': {
|
||||
'name': 'Demo User',
|
||||
'email': 'demo@test.com',
|
||||
'street': 'Rue des Bourlottes 9',
|
||||
'street2': '23',
|
||||
'country': 'BE',
|
||||
'city':'Ramillies',
|
||||
'zip':'1367'
|
||||
},
|
||||
}
|
||||
);
|
||||
const processingValues = await jsonrpc(
|
||||
this.paymentContext['transactionRoute'],
|
||||
this._prepareTransactionRouteParams(providerId),
|
||||
)
|
||||
paymentDemoMixin.processDemoPayment(processingValues);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Simulate a feedback from a payment provider and redirect the customer to the status page.
|
||||
*
|
||||
* @private
|
||||
* @param {object} processingValues - The processing values of the transaction.
|
||||
* @return {void}
|
||||
*/
|
||||
async processDemoPayment(processingValues) {
|
||||
const customerInput = document.getElementById('customer_input').value;
|
||||
const simulatedPaymentState = document.getElementById('simulated_payment_state').value;
|
||||
|
||||
jsonrpc('/payment/demo/simulate_payment', {
|
||||
'reference': processingValues.reference,
|
||||
'payment_details': customerInput,
|
||||
'simulated_state': simulatedPaymentState,
|
||||
}).then(() => {
|
||||
window.location = '/payment/status';
|
||||
}).guardedCatch(error => {
|
||||
error.event.preventDefault();
|
||||
this._displayErrorDialog(_t("Payment processing failed"), error.message.data.message);
|
||||
this._enableButton?.(); // This method doesn't exists in Express Checkout form.
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
import paymentForm from '@payment/js/payment_form';
|
||||
import paymentDemoMixin from '@payment_demo/js/payment_demo_mixin';
|
||||
|
||||
paymentForm.include({
|
||||
|
||||
@@ -48,20 +47,7 @@ paymentForm.include({
|
||||
this._super(...arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
const customerInput = document.getElementById('customer_input').value;
|
||||
const simulatedPaymentState = document.getElementById('simulated_payment_state').value;
|
||||
this.rpc('/payment/demo/simulate_payment', {
|
||||
'reference': processingValues.reference,
|
||||
'payment_details': customerInput,
|
||||
'simulated_state': simulatedPaymentState,
|
||||
}).then(() => {
|
||||
window.location = '/payment/status';
|
||||
}).guardedCatch(error => {
|
||||
error.event.preventDefault();
|
||||
this._displayErrorDialog(_t("Payment processing failed"), error.message.data.message);
|
||||
this._enableButton(); // The button has been disabled before initiating the flow.
|
||||
});
|
||||
paymentDemoMixin.processDemoPayment(processingValues);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -3,39 +3,7 @@
|
||||
|
||||
<template id="inline_form">
|
||||
<div t-attf-id="demo-container-{{provider_id}}">
|
||||
<div class="row gap-2 gap-md-0">
|
||||
<input name="provider_id" type="hidden" id="provider_id" t-att-value="id"/>
|
||||
<input name="partner_id" type="hidden" t-att-value="partner_id"/>
|
||||
<div class="col-12 col-md mt-0 mb-0">
|
||||
<label for="customer_input" class="mt-0 text-muted">
|
||||
<small>Payment Details (test data)</small>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="customer_input"
|
||||
id="customer_input"
|
||||
class="form-control"
|
||||
placeholder="XXXX XXXX XXXX XXXX"/>
|
||||
</div>
|
||||
<div class="col-12 col-md mb-0">
|
||||
<label for="simulated_payment_state" class="mt-0 text-muted">
|
||||
<small>Payment Status</small>
|
||||
</label>
|
||||
<select id="simulated_payment_state" class="form-select">
|
||||
<option value="done" title="Successful payment">
|
||||
Successful
|
||||
</option>
|
||||
<option value="pending" title="Payment processing">
|
||||
Pending
|
||||
</option>
|
||||
<option value="cancel" title="Payment canceled by customer">
|
||||
Canceled
|
||||
</option>
|
||||
<option value="error" title="Processing error">
|
||||
Error
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<t t-call="payment_demo.payment_details"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -58,4 +26,197 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="express_checkout_form">
|
||||
<div name="o_express_checkout_container"
|
||||
t-attf-id="o_demo_express_checkout_container_{{provider_sudo.id}}"
|
||||
t-att-data-provider-id="provider_sudo.id"
|
||||
t-att-data-provider-code="provider_sudo.code"
|
||||
>
|
||||
<button type="button"
|
||||
class="btn btn-primary mt-2 w-100"
|
||||
data-bs-toggle="modal"
|
||||
t-attf-data-bs-target="#o_payment_demo_modal_{{provider_sudo.id}}"
|
||||
>
|
||||
Pay with Demo
|
||||
<span t-if="not provider_sudo.is_published"
|
||||
class="badge rounded-pill text-bg-danger ms-1"
|
||||
>
|
||||
Unpublished
|
||||
</span>
|
||||
</button>
|
||||
<div t-attf-id="o_payment_demo_modal_{{provider_sudo.id}}"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
aria-labelledby="o_payment_demo_modal_label"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="o_payment_demo_modal_label">
|
||||
Demo Express Checkout
|
||||
</h5>
|
||||
<span t-if="provider_sudo.state == 'test'"
|
||||
class="badge rounded-pill text-bg-warning ms-1"
|
||||
>
|
||||
Test Mode
|
||||
</span>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<t t-call="payment_demo.express_inline_form">
|
||||
<t t-set="provider_id" t-value="provider_sudo.id"/>
|
||||
</t>
|
||||
<div class="float-end mt-2" t-att-data-provider-id="provider_sudo.id">
|
||||
<t t-call="payment.submit_button">
|
||||
<t t-set="submit_button_label">Pay</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="express_inline_form">
|
||||
<div>
|
||||
<t t-call="payment_demo.payment_details"/>
|
||||
<div t-if="shipping_info_required"
|
||||
t-attf-id="o_payment_demo_shipping_info_{{provider_id}}"
|
||||
>
|
||||
<t t-set="customer" t-value="request.env.user.partner_id"/>
|
||||
<div class="row mt-0">
|
||||
<div class="col mb-0">
|
||||
<label for="o_payment_demo_shipping_name" class=" mt-0">
|
||||
<small><b>Name</b></small>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="o_payment_demo_shipping_name"
|
||||
class="form-control"
|
||||
t-att-value="customer.name"
|
||||
readonly="1"
|
||||
required=""
|
||||
/>
|
||||
</div>
|
||||
<div class="col mb-0">
|
||||
<label for="o_payment_demo_shipping_email" class=" mt-0">
|
||||
<small>Email</small>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="o_payment_demo_shipping_email"
|
||||
class="form-control"
|
||||
t-att-value="customer.email or 'example@example.com'"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mb-0">
|
||||
<label for="o_payment_demo_shipping_address" class="mt-1">
|
||||
<small><b>Street and Number</b></small>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="o_payment_demo_shipping_address"
|
||||
class="form-control"
|
||||
required=""
|
||||
t-att-value="customer.street or 'Rue des Bourlottes 9'"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="col mb-0">
|
||||
<label for="o_payment_demo_shipping_address2" class="mt-1">
|
||||
<small>Street 2</small>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="o_payment_demo_shipping_address2"
|
||||
class="form-control"
|
||||
t-att-value="customer.street2"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mb-0">
|
||||
<label for="o_payment_demo_shipping_zip" class="mt-1">
|
||||
<small><b>Zip Code</b></small>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="o_payment_demo_shipping_zip"
|
||||
class="form-control"
|
||||
required=""
|
||||
t-att-value="customer.zip or '1367'"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="col mb-0">
|
||||
<label for="o_payment_demo_shipping_city" class="mt-1">
|
||||
<small><b>City</b></small>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="o_payment_demo_shipping_city"
|
||||
class="form-control"
|
||||
t-att-value="customer.city or 'Ramillies'"
|
||||
readonly="1"
|
||||
required=""
|
||||
/>
|
||||
</div>
|
||||
<div class="col mb-0">
|
||||
<label for="o_payment_demo_shipping_country" class="mt-1">
|
||||
<small><b>Country</b></small>
|
||||
</label>
|
||||
<select id="o_payment_demo_shipping_country"
|
||||
class="form-select"
|
||||
disabled="true"
|
||||
>
|
||||
<option t-att-value="customer.country_id.code or 'BE'"
|
||||
t-out="customer.country_id.name or 'Belgium'"
|
||||
/>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="payment_details">
|
||||
<div class="row gap2 gap-md-0">
|
||||
<input name="provider_id" type="hidden" id="provider_id" t-att-value="id"/>
|
||||
<input name="partner_id" type="hidden" t-att-value="partner_id"/>
|
||||
<div class="col-12 col-md mt-0 mb-0">
|
||||
<label for="customer_input" class="mt-0">
|
||||
<small>Payment Details (test data)</small>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="customer_input"
|
||||
id="customer_input"
|
||||
class="form-control"
|
||||
placeholder="XXXX XXXX XXXX XXXX"/>
|
||||
</div>
|
||||
<div class="col-12 col-md mb-0">
|
||||
<label for="simulated_payment_state" class="mt-0 text-muted">
|
||||
<small>Payment Status</small>
|
||||
</label>
|
||||
<select id="simulated_payment_state" class="form-select">
|
||||
<option value="done" title="Successful payment">
|
||||
Successful
|
||||
</option>
|
||||
<option value="pending" title="Payment processing">
|
||||
Pending
|
||||
</option>
|
||||
<option value="cancel" title="Payment canceled by customer">
|
||||
Canceled
|
||||
</option>
|
||||
<option value="error" title="Processing error">
|
||||
Error
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
type="object"
|
||||
name="action_stripe_verify_apple_pay_domain"
|
||||
class="btn btn-primary"
|
||||
invisible="not allow_express_checkout"/>
|
||||
invisible="not allow_express_checkout or code != 'stripe'"/>
|
||||
</div>
|
||||
</field>
|
||||
</field>
|
||||
|
||||
@@ -1312,7 +1312,8 @@ class WebsiteSale(payment_portal.PaymentPortal):
|
||||
# assigned to ensure the right behavior from `shop_payment_confirmation()`.
|
||||
request.session['sale_last_order_id'] = order_sudo.id
|
||||
|
||||
if shipping_address and shipping_option:
|
||||
if shipping_address:
|
||||
#in order to not override shippig address, it's checked separately from shipping option
|
||||
self._include_country_and_state_in_address(shipping_address)
|
||||
|
||||
if order_sudo.partner_shipping_id.name.endswith(order_sudo.name):
|
||||
@@ -1338,7 +1339,8 @@ class WebsiteSale(payment_portal.PaymentPortal):
|
||||
shipping_address, type='delivery', parent_id=order_sudo.partner_id.id
|
||||
)
|
||||
# Process the delivery carrier
|
||||
order_sudo._check_carrier_quotation(force_carrier_id=int(shipping_option['id']))
|
||||
if shipping_option:
|
||||
order_sudo._check_carrier_quotation(force_carrier_id=int(shipping_option['id']))
|
||||
|
||||
return order_sudo.partner_id.id
|
||||
|
||||
@@ -1373,7 +1375,7 @@ class WebsiteSale(payment_portal.PaymentPortal):
|
||||
('code', '=', address.pop('country')),
|
||||
], limit=1)
|
||||
state = request.env["res.country.state"].search([
|
||||
('code', '=', address.pop('state')),
|
||||
('code', '=', address.pop('state', '')),
|
||||
], limit=1)
|
||||
address.update(country_id=country, state_id=state)
|
||||
|
||||
@@ -1538,7 +1540,6 @@ class WebsiteSale(payment_portal.PaymentPortal):
|
||||
# ------------------------------------------------------
|
||||
|
||||
def _get_express_shop_payment_values(self, order, **kwargs):
|
||||
request.session['sale_last_order_id'] = order.id
|
||||
payment_form_values = sale_portal.CustomerPortal._get_payment_values(
|
||||
self, order, website_id=request.website.id, is_express_checkout=True
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user