[IMP] website_sale_aplicoop: pay on the checkout page, not a step later
The separate /eskaera/<slug>/payment step is gone. Members review the summary, choose home delivery and pick a payment method on the checkout, in one screen; the old URL redirects there so bookmarks and sessions that were mid-flow do not hit a 404. The checkout now renders the member's draft sale.order instead of the localStorage cart. That is what fixes the products appearing "out of nowhere" between the two pages: the summary was a snapshot of localStorage taken at page load, and `_autoLoadDraftOnInit` then pulled the draft back into localStorage without re-rendering. Deleting a product in the shop removed it from the cart but left the line on the draft, so the autoload resurrected it, the confirm button sent it back, and it only became visible one page later. The checkout no longer auto-loads the draft — it renders it, and what it shows is what the payment form charges. "Proceed to Checkout" pushes the cart to that draft before navigating. Saving is idempotent: `_merge_or_replace_draft` reuses the cycle's draft and, through the new `_draft_matches_lines`, rewrites `order_line` only when the lines actually differ — replacing them unlinks and recreates every one of them, which is pure churn when nothing changed. The home delivery checkbox goes through the new /eskaera/set-home-delivery so the delivery line moves on the order itself. Writing only to localStorage would have changed the summary and left the amount alone, which with online payment on is the amount being charged. Also fixes the confirmation notice nobody ever saw: saving answered with the payment step URL and the frontend followed it immediately, destroying the toast in the same tick. Saving no longer navigates; the caller decides whether it is staying or moving on. Along the way: checkout_labels.js and the eskaera_checkout_summary / eskaera_payment templates are removed, superseded by the server-rendered summary and checkout, and the stale sessionStorage delivery preference no longer overrides the checkbox the order just rendered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f81c1ca8e7
commit
e625b0c2f3
11 changed files with 873 additions and 746 deletions
|
|
@ -338,7 +338,10 @@
|
|||
<i class="fa fa-truck cart-icon-size" aria-hidden="true" />
|
||||
</button>
|
||||
</t>
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success cart-btn-compact" aria-label="Proceed to Checkout" title="Proceed to Checkout" data-bs-toggle="tooltip">
|
||||
<!-- js-eskaera-checkout: the cart is pushed to the draft
|
||||
sale.order before navigating, so the checkout page can
|
||||
render (and charge) the very lines the member sees. -->
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success cart-btn-compact js-eskaera-checkout" aria-label="Proceed to Checkout" title="Proceed to Checkout" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-check cart-icon-size" aria-hidden="true" />
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger cart-btn-compact" id="clear-cart-btn" t-attf-data-order-id="{{ group_order.id }}" title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
|
||||
|
|
@ -353,7 +356,7 @@
|
|||
<button type="button" class="btn btn-outline-danger btn-sm" id="clear-cart-btn-footer" t-attf-data-order-id="{{ group_order.id }}" title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
|
||||
<i class="fa fa-trash me-1" aria-hidden="true" />Clear Cart
|
||||
</button>
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success checkout-btn-lg" title="Proceed to Checkout" data-bs-toggle="tooltip">
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success checkout-btn-lg js-eskaera-checkout" title="Proceed to Checkout" data-bs-toggle="tooltip">
|
||||
Proceed to Checkout
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -428,41 +431,9 @@
|
|||
</script>
|
||||
</t>
|
||||
</template>
|
||||
<template id="eskaera_checkout_summary" name="Checkout Order Summary">
|
||||
<!-- The table keeps its width and scrolls inside this container; tabindex
|
||||
makes that scroll reachable from the keyboard (WCAG 2.1.1). -->
|
||||
<div class="checkout-summary-container" role="region" tabindex="0" aria-label="Order summary">
|
||||
<table class="table table-hover checkout-summary-table" id="checkout-summary-table">
|
||||
<caption class="visually-hidden">Products in this order, with quantity, price and subtotal</caption>
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th scope="col" class="col-name">Product</th>
|
||||
<th scope="col" class="col-qty text-center">Quantity</th>
|
||||
<th scope="col" class="col-price text-end">Price</th>
|
||||
<th scope="col" class="col-subtotal text-end">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="checkout-summary-tbody">
|
||||
<tr id="checkout-empty-row" class="empty-message">
|
||||
<td colspan="4" class="text-center text-muted py-4">
|
||||
<i class="fa fa-inbox fa-2x mb-2" aria-hidden="true" />
|
||||
<p>This order's cart is empty</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="checkout-total-section">
|
||||
<div class="total-row">
|
||||
<span class="total-label">Total</span>:
|
||||
<span class="total-amount" id="checkout-total-amount">0.00</span>
|
||||
<span class="currency">€</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="eskaera_checkout" name="Eskaera Checkout">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="eskaera-checkout-page oe_structure oe_empty" data-name="Eskaera Checkout" t-attf-data-delivery-product-id="{{ delivery_product_id }}" t-attf-data-delivery-product-name="{{ delivery_product_name }}" t-attf-data-delivery-product-price="{{ delivery_product_price }}" t-attf-data-home-delivery-enabled="{{ 'true' if group_order.home_delivery else 'false' }}" t-attf-data-pickup-day="{{ group_order.pickup_day }}" t-attf-data-pickup-date="{{ group_order.pickup_date.strftime('%d/%m/%Y') if group_order.pickup_date else '' }}" t-attf-data-next-pickup-slot-label="{{ group_order.next_pickup_slot_id.label if group_order.next_pickup_slot_id else '' }}" t-attf-data-delivery-notice="{{ (group_order.delivery_notice or '').replace(chr(10), ' ').replace(chr(13), ' ') }}">
|
||||
<div id="wrap" class="eskaera-checkout-page oe_structure oe_empty" data-name="Eskaera Checkout" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-delivery-product-id="{{ delivery_product_id }}" t-attf-data-delivery-product-name="{{ delivery_product_name }}" t-attf-data-delivery-product-price="{{ delivery_product_price }}" t-attf-data-home-delivery-enabled="{{ 'true' if group_order.home_delivery else 'false' }}" t-attf-data-pickup-day="{{ group_order.pickup_day }}" t-attf-data-pickup-date="{{ group_order.pickup_date.strftime('%d/%m/%Y') if group_order.pickup_date else '' }}" t-attf-data-next-pickup-slot-label="{{ group_order.next_pickup_slot_id.label if group_order.next_pickup_slot_id else '' }}" t-attf-data-delivery-notice="{{ (group_order.delivery_notice or '').replace(chr(10), ' ').replace(chr(13), ' ') }}">
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 offset-lg-1">
|
||||
|
|
@ -536,49 +507,122 @@
|
|||
</div>
|
||||
<h4 class="summary-heading mb-3">Order Summary</h4>
|
||||
<div class="oe_structure oe_empty mb-3" data-name="Before Summary" />
|
||||
<!-- The summary comes from the saved sale.order, never from
|
||||
the localStorage cart: these are the very lines and
|
||||
amounts the payment form is about to charge, so the two
|
||||
cannot drift apart. The shop pushes the cart here before
|
||||
sending the member over. -->
|
||||
<div id="checkout-summary" class="mb-5">
|
||||
<t t-call="website_sale_aplicoop.eskaera_checkout_summary">
|
||||
<t t-set="labels" t-value="{}" />
|
||||
<t t-if="sale_order">
|
||||
<t t-call="website_sale_aplicoop.eskaera_order_lines_summary" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info" role="status">
|
||||
<i class="fa fa-inbox me-2" aria-hidden="true" t-translation="off" />
|
||||
<span>This order's cart is empty</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="oe_structure oe_empty mb-4" data-name="After Summary" />
|
||||
<t t-if="group_order.home_delivery">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="form-check eskaera-delivery-check">
|
||||
<input type="checkbox" class="form-check-input" id="home-delivery-checkbox" name="home_delivery" />
|
||||
<label class="form-check-label fw-bold" for="home-delivery-checkbox">Home Delivery</label>
|
||||
<t t-if="sale_order">
|
||||
<t t-if="group_order.home_delivery">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="form-check eskaera-delivery-check">
|
||||
<input type="checkbox" class="form-check-input" id="home-delivery-checkbox" name="home_delivery" t-att-checked="sale_order.home_delivery or None" />
|
||||
<label class="form-check-label fw-bold" for="home-delivery-checkbox">Home Delivery</label>
|
||||
</div>
|
||||
<div t-attf-class="alert alert-info mt-3 eskaera-delivery-notice{{ '' if sale_order.home_delivery else ' d-none' }}" id="delivery-info-alert">
|
||||
<p class="mb-2">
|
||||
<i class="fa fa-truck" aria-hidden="true" t-translation="off" />
|
||||
<t t-if="group_order.delivery_date">
|
||||
<strong>Delivery Information:</strong> Your order will be delivered at
|
||||
<t t-esc="day_names[(int(group_order.pickup_day) + 1) % 7]" />
|
||||
<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />
|
||||
<t t-if="group_order.delivery_notice">
|
||||
<br />
|
||||
<t t-esc="group_order.delivery_notice" />
|
||||
</t>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="delivery-info-alert" class="alert alert-info mt-3 d-none eskaera-delivery-notice">
|
||||
<p class="mb-2">
|
||||
<i class="fa fa-truck" aria-hidden="true" t-translation="off" />
|
||||
<t t-if="group_order.delivery_date">
|
||||
<strong>Delivery Information:</strong> Your order will be delivered at
|
||||
<t t-esc="day_names[(int(group_order.pickup_day) + 1) % 7]" />
|
||||
<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />
|
||||
<t t-if="group_order.delivery_notice">
|
||||
<br />
|
||||
<t t-esc="group_order.delivery_notice" />
|
||||
</t>
|
||||
</t>
|
||||
</p>
|
||||
</t>
|
||||
<t t-if="online_payment">
|
||||
<t t-if="pending_transaction">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fa fa-clock-o me-2" aria-hidden="true" t-translation="off" />
|
||||
<span>Payment in progress</span>
|
||||
</h5>
|
||||
<p class="mb-1">We are still waiting for your payment to be confirmed. Please do not pay again.</p>
|
||||
<p class="mb-0">
|
||||
<span>Reference</span>:
|
||||
<span t-esc="pending_transaction.reference" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="checkout-actions d-grid gap-3">
|
||||
<a t-att-href="sale_order.get_portal_url()" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="fa fa-file-text-o" aria-hidden="true" t-translation="off" />
|
||||
<span>View my order</span>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="payment_ready and payment_available">
|
||||
<h4 class="summary-heading mb-3">Payment Method</h4>
|
||||
<!-- The whole provider UI is payment.form; this addon
|
||||
configures no provider of its own. -->
|
||||
<div id="payment_method" class="o_not_editable mb-4">
|
||||
<t t-call="payment.form" />
|
||||
</div>
|
||||
<!-- Deliberately outside #o_payment_form: website_sale's
|
||||
payment_form.js binds every
|
||||
[name="o_payment_submit_button"] in the document, on
|
||||
top of payment_form.js's own delegated handler inside
|
||||
the form. A button inside would get both listeners and
|
||||
one click would open two transactions. -->
|
||||
<div class="checkout-actions d-grid gap-3">
|
||||
<t t-call="payment.submit_button" />
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||||
<span>Back to Cart</span>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-warning" role="alert" t-esc="no_payment_method_message" />
|
||||
<div class="checkout-actions d-grid gap-3">
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||||
<span>Back to Cart</span>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<!-- No online payment: the draft is confirmed in bulk by
|
||||
the cutoff cron, so the button only re-saves the cart. -->
|
||||
<div class="checkout-actions d-grid gap-3" id="checkout-form-labels">
|
||||
<button class="btn btn-success btn-lg" id="confirm-order-btn" t-attf-data-order-id="{{ group_order.id }}" t-att-data-confirmed-label="checkout_button['done_label']" t-att-data-pickup-label="labels.get('pickup_day_label', 'Pickup Day')" t-att-aria-label="checkout_button['hint']" t-att-data-bs-title="checkout_button['label']" t-att-data-tooltip-key="checkout_button['tooltip_key']" data-bs-toggle="tooltip">
|
||||
<i t-attf-class="fa {{ checkout_button['icon'] }}" aria-hidden="true" t-translation="off" />
|
||||
<span t-esc="checkout_button['label']" />
|
||||
</button>
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||||
<span>Back to Cart</span>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="checkout-actions d-grid gap-3">
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-success btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||||
<span>Back to Cart</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="online_payment and not payment_available">
|
||||
<div class="alert alert-warning" role="alert" t-esc="no_payment_method_message" />
|
||||
</t>
|
||||
<div class="checkout-actions d-grid gap-3" id="checkout-form-labels">
|
||||
<button class="btn btn-success btn-lg" id="confirm-order-btn" t-attf-data-order-id="{{ group_order.id }}" t-att-data-confirmed-label="checkout_button['done_label']" t-att-data-pickup-label="labels.get('pickup_day_label', 'Pickup Day')" t-att-aria-label="checkout_button['hint']" t-att-data-bs-title="checkout_button['label']" t-att-data-tooltip-key="checkout_button['tooltip_key']" t-att-disabled="online_payment and not payment_available" data-bs-toggle="tooltip">
|
||||
<i t-attf-class="fa {{ checkout_button['icon'] }}" aria-hidden="true" t-translation="off" />
|
||||
<span t-esc="checkout_button['label']" />
|
||||
</button>
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||||
<span>Back to Cart</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -593,45 +637,14 @@
|
|||
console.log('[LABELS] Initialized from server:', window.groupOrderShop.labels);
|
||||
})();
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
// Auto-load cart from localStorage when accessing checkout directly
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Get order ID from button
|
||||
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||
if (!confirmBtn) return;
|
||||
|
||||
var orderId = confirmBtn.getAttribute('data-order-id');
|
||||
var cartKey = 'eskaera_' + orderId + '_cart';
|
||||
|
||||
// Check if there's a saved cart and load it
|
||||
var savedCart = localStorage.getItem(cartKey);
|
||||
if (savedCart) {
|
||||
try {
|
||||
var cart = JSON.parse(savedCart);
|
||||
console.log('[CHECKOUT AUTO-LOAD] Cart found in localStorage:', cart);
|
||||
|
||||
// Simulate cart loading by triggering a custom event
|
||||
// The checkout_labels.js will listen for cart data
|
||||
var event = new CustomEvent('cartLoaded', { detail: { cart: cart } });
|
||||
document.dispatchEvent(event);
|
||||
} catch (e) {
|
||||
console.error('[CHECKOUT AUTO-LOAD] Error parsing cart:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('[CHECKOUT AUTO-LOAD] No cart found in localStorage');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="eskaera_order_lines_summary" name="Placed Order Summary">
|
||||
<!-- Server-side twin of eskaera_checkout_summary: from the payment
|
||||
step on, the amounts shown must be the ones that will be
|
||||
charged, so they come from the sale.order and not from the
|
||||
localStorage cart. -->
|
||||
<!-- The order summary of every page past the shop. The cart is a
|
||||
localStorage object while the member shops, but from the
|
||||
checkout on the amounts shown must be the ones that will be
|
||||
charged, so they come from the sale.order. -->
|
||||
<div class="checkout-summary-container" role="region" tabindex="0" aria-label="Order summary">
|
||||
<table class="table table-hover checkout-summary-table">
|
||||
<caption class="visually-hidden">Products in this order, with quantity, price and subtotal</caption>
|
||||
|
|
@ -662,69 +675,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="eskaera_payment" name="Eskaera Payment">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="eskaera-checkout-page oe_structure oe_empty" data-name="Eskaera Payment">
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 offset-lg-1">
|
||||
<div class="mb-4">
|
||||
<t t-call="website_sale_aplicoop.order_header">
|
||||
<t t-set="header_class" t-value="'checkout-header'" />
|
||||
<t t-set="header_title">Pay Order: <t t-esc="group_order.name" />
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<h4 class="summary-heading mb-3">Order Summary</h4>
|
||||
<div class="mb-5">
|
||||
<t t-call="website_sale_aplicoop.eskaera_order_lines_summary" />
|
||||
</div>
|
||||
<t t-if="pending_transaction">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fa fa-clock-o me-2" aria-hidden="true" t-translation="off" />
|
||||
<span>Payment in progress</span>
|
||||
</h5>
|
||||
<p class="mb-1">We are still waiting for your payment to be confirmed. Please do not pay again.</p>
|
||||
<p class="mb-0">
|
||||
<span>Reference</span>:
|
||||
<span t-esc="pending_transaction.reference" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="checkout-actions d-grid gap-3">
|
||||
<a t-att-href="sale_order.get_portal_url()" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="fa fa-file-text-o" aria-hidden="true" t-translation="off" />
|
||||
<span>View my order</span>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<h4 class="summary-heading mb-3">Payment Method</h4>
|
||||
<!-- The whole provider UI is payment.form; this addon
|
||||
configures no provider of its own. -->
|
||||
<div id="payment_method" class="o_not_editable mb-4">
|
||||
<t t-call="payment.form" />
|
||||
</div>
|
||||
<!-- Deliberately outside #o_payment_form: website_sale's
|
||||
payment_form.js binds every
|
||||
[name="o_payment_submit_button"] in the document, on
|
||||
top of payment_form.js's own delegated handler inside
|
||||
the form. A button inside would get both listeners and
|
||||
one click would open two transactions. -->
|
||||
<div class="checkout-actions d-grid gap-3">
|
||||
<t t-call="payment.submit_button" />
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||||
<span>Back to Checkout</span>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="eskaera_payment_confirmation" name="Eskaera Payment Confirmation">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="eskaera-checkout-page oe_structure oe_empty" data-name="Eskaera Payment Confirmation" t-attf-data-clear-cart-order-id="{{ group_order.id }}">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue