[ADD] website_sale_aplicoop: botón limpiar carrito en sidebar

Añade botón 'Clear Cart' (fa-trash) en el header y footer del sidebar
del carrito en la página de lista de productos.

Cambios:
- views/website_templates.xml: botón clear-cart-btn en card-header y
  clear-cart-btn-footer en card-footer del sidebar
- controllers/website_sale.py: nuevo endpoint POST /eskaera/clear-cart
  que cancela el sale.order borrador del usuario si existe
- static/src/js/website_sale.js: método _clearCart(), listeners para
  ambos botones (header + footer)
- models/js_translations.py: nuevas cadenas clear_cart, clear_cart_confirm,
  cart_cleared, draft_cancelled
- i18n/es.po, i18n/eu.po: traducciones ES y EU de los nuevos labels
This commit is contained in:
snt 2026-04-07 23:50:30 +02:00
parent 0eb7957a70
commit 135967019e
6 changed files with 244 additions and 7 deletions

View file

@ -268,6 +268,13 @@ class AplicoopWebsiteSale(WebsiteSale):
"items": env_lang._("items"), "items": env_lang._("items"),
"added_to_cart": env_lang._("added to cart"), "added_to_cart": env_lang._("added to cart"),
"out_of_stock": env_lang._("Out of stock"), "out_of_stock": env_lang._("Out of stock"),
# ============ CLEAR CART ============
"clear_cart": env_lang._("Clear Cart"),
"clear_cart_confirm": env_lang._(
"Are you sure you want to clear the cart? This will also cancel any saved draft order."
),
"cart_cleared": env_lang._("Cart cleared"),
"draft_cancelled": env_lang._("draft order cancelled"),
} }
return labels return labels
@ -2415,6 +2422,107 @@ class AplicoopWebsiteSale(WebsiteSale):
status=500, status=500,
) )
@http.route(
["/eskaera/clear-cart"],
type="http",
auth="user",
website=True,
methods=["POST"],
csrf=False,
)
def clear_cart(self, **post):
"""Clear the user's cart and cancel any existing draft sale.order.
Receives: JSON body with 'order_id'
Returns: JSON with success status and cancelled_order_id if applicable.
"""
try:
data = self._decode_json_body()
except ValueError as e:
return request.make_response(
json.dumps({"error": str(e)}),
[("Content-Type", "application/json")],
status=400,
)
order_id = data.get("order_id")
if not order_id:
return request.make_response(
json.dumps({"error": "order_id is required"}),
[("Content-Type", "application/json")],
status=400,
)
try:
order_id = int(order_id)
except (ValueError, TypeError):
return request.make_response(
json.dumps({"error": f"Invalid order_id format: {order_id}"}),
[("Content-Type", "application/json")],
status=400,
)
group_order = request.env["group.order"].sudo().browse(order_id)
if not group_order.exists():
return request.make_response(
json.dumps({"error": f"Order {order_id} not found"}),
[("Content-Type", "application/json")],
status=404,
)
current_user = request.env.user
if not current_user.partner_id:
return request.make_response(
json.dumps({"error": "User has no associated partner"}),
[("Content-Type", "application/json")],
status=400,
)
# Find and cancel any existing draft sale.order for this period
cancelled_order_id = None
try:
draft_orders = self._find_recent_draft_order(
current_user.partner_id.id, group_order
)
if draft_orders:
draft_order = draft_orders[0]
cancelled_order_id = draft_order.id
# Use action_cancel if available, otherwise unlink
if draft_order.state == "draft":
draft_order.sudo().action_cancel()
_logger.info(
"clear_cart: Cancelled draft sale.order %d for partner %d",
draft_order.id,
current_user.partner_id.id,
)
else:
_logger.info(
"clear_cart: Draft order %d already in state '%s', skipping cancel",
draft_order.id,
draft_order.state,
)
except Exception as e:
_logger.warning(
"clear_cart: Error cancelling draft order for partner %d: %s",
current_user.partner_id.id,
str(e),
)
response_data = {
"success": True,
"message": request.env._("Cart cleared"),
"cancelled_order_id": cancelled_order_id,
}
_logger.info(
"clear_cart: Cart cleared for partner %d, order %d (cancelled_sale_order: %s)",
current_user.partner_id.id,
order_id,
cancelled_order_id,
)
return request.make_response(
json.dumps(response_data),
[("Content-Type", "application/json")],
)
@http.route( @http.route(
["/eskaera/save-order"], ["/eskaera/save-order"],
type="http", type="http",

View file

@ -1243,12 +1243,12 @@ msgstr "Guardar como Borrador"
#. odoo-python #. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Save Draft" msgid "Save Draft"
msgstr "Guardar borrador" msgstr "Confirmar Pedido"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout #: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout
msgid "Save order as draft" msgid "Save order as draft"
msgstr "Guardar pedido como borrador" msgstr "Confirmar Pedido"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#. odoo-python #. odoo-python
@ -2278,3 +2278,22 @@ msgstr "Semanal"
msgid "Whether this picking includes home delivery (from sale order)" msgid "Whether this picking includes home delivery (from sale order)"
msgstr "Si este albarán incluye entrega a domicilio (del pedido de venta)" msgstr "Si este albarán incluye entrega a domicilio (del pedido de venta)"
#. module: website_sale_aplicoop
msgid "Clear Cart"
msgstr "Vaciar carrito"
#. module: website_sale_aplicoop
msgid ""
"Are you sure you want to clear the cart? This will also cancel any saved "
"draft order."
msgstr ""
"¿Estás seguro de que quieres vaciar el carrito? Esto también cancelará "
"cualquier pedido borrador guardado."
#. module: website_sale_aplicoop
msgid "Cart cleared"
msgstr "Carrito vaciado"
#. module: website_sale_aplicoop
msgid "draft order cancelled"
msgstr "pedido borrador cancelado"

View file

@ -1242,12 +1242,12 @@ msgstr "Zirriborro Gisa Gorde"
#. odoo-python #. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Save Draft" msgid "Save Draft"
msgstr "Zirriborroa gorde" msgstr "Eskaera Konfirmatu"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout #: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout
msgid "Save order as draft" msgid "Save order as draft"
msgstr "Gorde enparatzea zirriborro gisa" msgstr "Eskaera Zirriborro Gisa Gorde"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#. odoo-python #. odoo-python
@ -2272,3 +2272,21 @@ msgstr ""
"eskaeraren)" "eskaeraren)"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
msgid "Clear Cart"
msgstr "Saskia garbitu"
#. module: website_sale_aplicoop
msgid ""
"Are you sure you want to clear the cart? This will also cancel any saved "
"draft order."
msgstr ""
"Ziur zaude saskia garbitu nahi duzula? Honek gordetako zirriborro-eskaera "
"ere ezeztatuko du."
#. module: website_sale_aplicoop
msgid "Cart cleared"
msgstr "Saskia garbituta"
#. module: website_sale_aplicoop
msgid "draft order cancelled"
msgstr "zirriborro-eskaera ezeztatu da"

View file

@ -173,3 +173,13 @@ def _register_translations():
_("Friday") _("Friday")
_("Saturday") _("Saturday")
_("Sunday") _("Sunday")
# ========================
# Clear Cart
# ========================
_("Clear Cart")
_(
"Are you sure you want to clear the cart? This will also cancel any saved draft order."
)
_("Cart cleared")
_("draft order cancelled")

View file

@ -748,6 +748,22 @@
}); });
} }
// Buttons to clear cart (header + footer)
var clearCartBtns = [
document.getElementById("clear-cart-btn"),
document.getElementById("clear-cart-btn-footer"),
];
clearCartBtns.forEach(function (btn) {
if (btn) {
console.log("[_attachEventListeners] clear-cart-btn found:", btn.id);
btn.addEventListener("click", function (e) {
console.log("[CLICK] clear-cart-btn clicked");
e.preventDefault();
self._clearCart();
});
}
});
this._cartCheckoutListenersAttached = true; this._cartCheckoutListenersAttached = true;
console.log("[_attachEventListeners] Checkout listeners attached (one-time)"); console.log("[_attachEventListeners] Checkout listeners attached (one-time)");
} }
@ -1029,6 +1045,12 @@
draft_replace_warning: "The existing draft will be permanently deleted.", draft_replace_warning: "The existing draft will be permanently deleted.",
draft_merge_btn: "Merge", draft_merge_btn: "Merge",
draft_replace_btn: "Replace", draft_replace_btn: "Replace",
// Clear cart labels
clear_cart: "Clear Cart",
clear_cart_confirm:
"Are you sure you want to clear the cart? This will also cancel any saved draft order.",
cart_cleared: "Cart cleared",
draft_cancelled: "draft order cancelled",
}; };
}, },
@ -1636,6 +1658,60 @@
xhr.send(JSON.stringify(orderData)); xhr.send(JSON.stringify(orderData));
}, },
_clearCart: function () {
var self = this;
var labels = this._getLabels();
var confirmMsg =
labels.clear_cart_confirm ||
"Are you sure you want to clear the cart? This will also cancel any saved draft order.";
if (!window.confirm(confirmMsg)) {
return;
}
// Clear localStorage immediately
this._clearCurrentOrderCartSilently();
this._updateCartDisplay();
// Cancel draft sale.order on the server
var xhr = new XMLHttpRequest();
xhr.open("POST", "/eskaera/clear-cart", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = function () {
var labels2 = self._getLabels();
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.success) {
var msg = labels2.cart_cleared || "Cart cleared";
if (data.cancelled_order_id) {
msg +=
" (" +
(labels2.draft_cancelled || "draft order cancelled") +
")";
}
self._showNotification("✓ " + msg, "success", 4000);
}
} catch (e) {
console.error("[_clearCart] Error parsing response:", e);
}
} else {
console.warn("[_clearCart] Server error:", xhr.status, xhr.responseText);
// Cart is already cleared locally, server error is non-critical
self._showNotification(labels2.cart_cleared || "Cart cleared", "success", 3000);
}
};
xhr.onerror = function () {
console.warn("[_clearCart] Network error, but cart already cleared locally");
var labels2 = self._getLabels();
self._showNotification(labels2.cart_cleared || "Cart cleared", "success", 3000);
};
xhr.send(JSON.stringify({ order_id: this.orderId }));
},
_saveOrderDraft: function () { _saveOrderDraft: function () {
console.log("[_saveOrderDraft] Starting - this.orderId:", this.orderId); console.log("[_saveOrderDraft] Starting - this.orderId:", this.orderId);

View file

@ -283,12 +283,18 @@
<a t-attf-href="/eskaera/{{ group_order.id }}/checkout" class="btn btn-success cart-btn-compact" aria-label="Proceed to checkout" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip"> <a t-attf-href="/eskaera/{{ group_order.id }}/checkout" class="btn btn-success cart-btn-compact" aria-label="Proceed to checkout" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip">
<i class="fa fa-check cart-icon-size" aria-hidden="true" /> <i class="fa fa-check cart-icon-size" aria-hidden="true" />
</a> </a>
<button type="button" class="btn btn-outline-danger cart-btn-compact" id="clear-cart-btn" t-attf-data-order-id="{{ group_order.id }}" data-bs-title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
<i class="fa fa-trash cart-icon-size" aria-hidden="true" />
</button>
</div> </div>
</div> </div>
<div class="card-body cart-body-lg" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals"> <div class="card-body cart-body-lg" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
<p class="text-muted">This order's cart is empty</p> <p class="text-muted">This order's cart is empty</p>
</div> </div>
<div class="card-footer bg-white text-center"> <div class="card-footer bg-white d-flex justify-content-between align-items-center gap-2">
<button type="button" class="btn btn-outline-danger btn-sm" id="clear-cart-btn-footer" t-attf-data-order-id="{{ group_order.id }}" data-bs-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.id }}/checkout" class="btn btn-success checkout-btn-lg" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip"> <a t-attf-href="/eskaera/{{ group_order.id }}/checkout" class="btn btn-success checkout-btn-lg" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip">
Proceed to Checkout Proceed to Checkout
</a> </a>