[ADD] website_sale_aplicoop: online payment per group order
Members can now pay their eskaera at checkout, through the standard Odoo payment machinery. Enabled per group order with a new `online_payment` boolean, off by default: an order without it behaves exactly as before, members save a draft and the cutoff cron confirms them in bulk. The flow mirrors website_sale's: the checkout button becomes "Confirm and pay", saving the cart redirects to a new /eskaera/<slug>/payment step that renders `payment.form` from `sale`'s `_get_payment_values`, and the standard /my/orders/<id>/transaction route takes it from there. This addon ships no provider and configures none; the co-op publishes whichever it wants. `website_sale`'s `_get_shop_payment_values` is deliberately not reused: it runs `_get_shop_payment_errors`, which blocks on shippable products without a delivery method — exactly an eskaera order, collected at the co-op with no carrier. For the same reason the transaction route stays the portal one, which does not call `_check_cart_is_ready_to_be_paid()`. Payment confirms the order, which has three consequences handled here: * `payment.transaction._check_amount_and_confirm_order` now confirms group orders with `from_orderpoint=True`, the way the cutoff cron already does. Without it a product with a broken replenishment route raises inside `_post_process`, and `/payment/status/poll` rolls back and re-raises: the member sees a payment error over a `done` transaction and the retry cron fails forever. * `_confirm_linked_sale_orders` also sweeps the cycle's already confirmed orders into the picking batch, scoped by `pickup_date`. Its early return on "no drafts" ran before any batching, so a fully prepaid cycle produced no batch at all. `_cron_batch_paid_orders_of_closed_cycles` covers the same hole for cycles closed by hand. * A duplicate-order guard answers 409 on save-order, add-to-cart and load-draft, and shows a notice on the shop, so a member whose order is already placed cannot build and pay for a second one. The payment policy lives in the model rather than the controller: there are three sale.order creation paths and two are live, so `_compute_require_payment` and `_compute_prepayment_percent` are extended instead of patching five vals dicts. Orders are also created under the group order's company, which is what filters the payment providers. Along the way: eskaera drafts were invisible in /my/orders. The portal rule is `message_partner_ids child_of` and sale.order only subscribes the customer on send or confirm, never on a draft create, so the `_prepare_orders_domain` override that includes drafts never had any effect. Fixed with an explicit `message_subscribe`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a67181ab42
commit
6ba554c91b
21 changed files with 1993 additions and 37 deletions
35
website_sale_aplicoop/static/src/js/eskaera_payment.js
Normal file
35
website_sale_aplicoop/static/src/js/eskaera_payment.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2026 Criptomart
|
||||
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
*
|
||||
* Frees the localStorage cart once the order has been placed and paid.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var page = document.querySelector("[data-clear-cart-order-id]");
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
|
||||
var orderId = page.getAttribute("data-clear-cart-order-id");
|
||||
if (!orderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Client side only. /eskaera/clear-cart would also cancel the sale
|
||||
// order, which is exactly the wrong thing to do to an order that was
|
||||
// just paid for.
|
||||
try {
|
||||
localStorage.removeItem("eskaera_" + orderId + "_cart");
|
||||
localStorage.removeItem("eskaera_" + orderId + "_cart_cycle");
|
||||
} catch (e) {
|
||||
// localStorage unavailable (private mode, quota). The cart is
|
||||
// server-side irrelevant at this point; the duplicate-order guard
|
||||
// is what actually protects the member.
|
||||
console.warn("[ESKAERA PAYMENT] Could not clear the local cart:", e);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -592,6 +592,38 @@
|
|||
}
|
||||
},
|
||||
|
||||
// The member already has a placed order for this cycle (409). Their
|
||||
// local cart is stale, so drop it and send them to that order rather
|
||||
// than let them build — and pay for — a duplicate.
|
||||
_handleAlreadyPlacedResponse: function (xhr) {
|
||||
if (!xhr || xhr.status !== 409) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var data;
|
||||
try {
|
||||
data = JSON.parse(xhr.responseText || "{}");
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
if (!data.already_placed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var labels = this._getLabels();
|
||||
this._clearCurrentOrderCartSilently();
|
||||
this._updateCartDisplay();
|
||||
this._showNotification(
|
||||
data.error || labels.already_placed || "You already placed an order.",
|
||||
"warning",
|
||||
6000
|
||||
);
|
||||
if (data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_checkGroupOrderStatus: function (callback) {
|
||||
var self = this;
|
||||
var done = function () {
|
||||
|
|
@ -783,8 +815,18 @@
|
|||
var tooltipText = null;
|
||||
var labelKey = null;
|
||||
|
||||
// An explicit key on the element wins over the static map: the
|
||||
// checkout button carries a different label depending on
|
||||
// whether the group order takes online payments, and the
|
||||
// server is the one that knows.
|
||||
var declaredKey = element.getAttribute("data-tooltip-key");
|
||||
if (declaredKey && labels[declaredKey]) {
|
||||
labelKey = declaredKey;
|
||||
tooltipText = labels[declaredKey];
|
||||
}
|
||||
|
||||
// Check ID-based mapping
|
||||
if (element.id && tooltipMap[element.id]) {
|
||||
if (!tooltipText && element.id && tooltipMap[element.id]) {
|
||||
labelKey = tooltipMap[element.id];
|
||||
tooltipText = labels[labelKey];
|
||||
}
|
||||
|
|
@ -1790,6 +1832,9 @@
|
|||
self._updateCartDisplay();
|
||||
return;
|
||||
}
|
||||
if (self._handleAlreadyPlacedResponse(xhr)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
self._showNotification(
|
||||
|
|
@ -1914,6 +1959,9 @@
|
|||
self._updateCartDisplay();
|
||||
return;
|
||||
}
|
||||
if (self._handleAlreadyPlacedResponse(xhr)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
self._showNotification(
|
||||
|
|
@ -2036,10 +2084,17 @@
|
|||
|
||||
if (data.success) {
|
||||
var successMsg =
|
||||
data.message ||
|
||||
labels.draft_saved_success ||
|
||||
labels.draft_saved ||
|
||||
"Order saved as draft successfully";
|
||||
self._showNotification("\u2713 " + successMsg, "success", 5000);
|
||||
// With online payment on, the server answers with the
|
||||
// payment step URL: saving the cart is only half of
|
||||
// placing the order.
|
||||
if (data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
} else {
|
||||
self._showNotification(
|
||||
"Error: " + (data.error || labels.error_unknown || "Unknown error"),
|
||||
|
|
@ -2056,6 +2111,9 @@
|
|||
self._updateCartDisplay();
|
||||
return;
|
||||
}
|
||||
if (self._handleAlreadyPlacedResponse(xhr)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
console.error("HTTP error:", xhr.status, errorData);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue