[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:
Anita (anko)
2023-09-29 08:07:00 +00:00
committed by Valentin Chevalier
parent a1d670ca30
commit c375b8f534
9 changed files with 319 additions and 55 deletions
@@ -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>
+5 -4
View File
@@ -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
)