diff --git a/website_sale_aplicoop/CHANGELOG.md b/website_sale_aplicoop/CHANGELOG.md index 7a3f011..cba0818 100644 --- a/website_sale_aplicoop/CHANGELOG.md +++ b/website_sale_aplicoop/CHANGELOG.md @@ -1,36 +1,5 @@ # Changelog - Website Sale Aplicoop -## [18.0.1.16.0] - 2026-08-17 - -### Changed - -- **Payment moved into the checkout**: the separate `/eskaera//payment` - step is gone. Members review the summary, pick home delivery and choose a - payment method on `/eskaera//checkout`, in one screen. The old URL - redirects there so bookmarks and in-flight sessions keep working. -- **The checkout summary is server-side**: it renders the draft `sale.order` - instead of the localStorage cart, so what is shown is what gets charged. - "Proceed to Checkout" pushes the cart to that draft before navigating, - reusing the cycle's draft and rewriting its lines only when they differ. -- The home delivery checkbox on the checkout writes to the order through - `/eskaera/set-home-delivery`, so the total and the payment amount follow it. - -### Fixed - -- The confirmation notice after saving a draft was destroyed by the immediate - redirect to the payment step, so members saw nothing at all. Saving no longer - navigates. -- Products the member had deleted reappeared between the checkout and the - payment page. The checkout auto-loaded the draft into localStorage after the - summary had already rendered, resurrecting deleted lines, which were then - sent back on confirm. The checkout no longer auto-loads the draft — it - renders it. - -### Removed - -- `checkout_labels.js` and the `eskaera_checkout_summary` / `eskaera_payment` - templates, superseded by the server-rendered summary and checkout. - ## [18.0.1.13.0] - 2026-08-11 ### Added diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index e908de8..97c2a0e 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -3,7 +3,7 @@ { # noqa: B018 "name": "Website Sale - Aplicoop", - "version": "18.0.1.16.0", + "version": "18.0.1.15.0", "category": "Website/Sale", "summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders", "author": "Odoo Community Association (OCA), Criptomart", @@ -71,6 +71,7 @@ "website_sale_aplicoop/static/src/js/i18n_helpers.js", # Core shop functionality "website_sale_aplicoop/static/src/js/website_sale.js", + "website_sale_aplicoop/static/src/js/checkout_labels.js", "website_sale_aplicoop/static/src/js/home_delivery.js", "website_sale_aplicoop/static/src/js/eskaera_payment.js", # Search and pagination diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 5254423..8423255 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -526,39 +526,6 @@ class AplicoopWebsiteSale(WebsiteSale): product_max_qty[p.id] = net return products_ctx, product_max_qty - def _draft_matches_lines(self, draft, sale_order_lines, home_delivery): - """Whether `draft` already holds exactly `sale_order_lines`. - - The cart is pushed to the draft on every step of the flow (saving from - the shop, entering the checkout), so most of those writes have nothing - to change. Rewriting `order_line` anyway would unlink and recreate - every line for no reason — it moves their ids, bumps `write_date` and - drops anything attached to them. - """ - if draft.home_delivery != home_delivery: - return False - if len(draft.order_line) != len(sale_order_lines): - return False - - currency = draft.currency_id - - def _key(product_id, qty, price): - return (product_id, round(qty, 3), currency.round(price)) - - current = sorted( - _key(line.product_id.id, line.product_uom_qty, line.price_unit) - for line in draft.order_line - ) - incoming = sorted( - _key( - vals["product_id"], - vals["product_uom_qty"], - vals["price_unit"], - ) - for _command, _id, vals in sale_order_lines - ) - return current == incoming - def _merge_or_replace_draft( self, group_order, @@ -588,14 +555,6 @@ class AplicoopWebsiteSale(WebsiteSale): if existing_drafts: draft = existing_drafts[0].sudo() - if self._draft_matches_lines( - draft, sale_order_lines, effective_home_delivery - ): - _logger.info( - "Draft order %s already matches the cart, left untouched", - draft.id, - ) - return draft _logger.info( "Replacing existing draft order %s for partner %s", draft.id, @@ -1268,13 +1227,36 @@ class AplicoopWebsiteSale(WebsiteSale): self._eskaera_payment_confirmation_url(group_order, placed_order) ) - # The cart itself still lives in localStorage while the member shops, - # but from here on everything is read off their draft sale.order: the - # checkout shows the very lines and amounts that are about to be paid. - # The shop pushes the cart to that draft before sending them here. - order_sudo = self._find_recent_draft_order( - request.env.user.partner_id.id, group_order + # Los datos del carrito vienen desde localStorage en el frontend + # Esta página solo muestra resumen y botón de confirmación + + # DEBUG: Log ALL delivery fields + _logger.warning("=== ESKAERA_CHECKOUT DELIVERY DEBUG ===") + _logger.warning("group_order.id: %s", group_order.id) + _logger.warning("group_order.name: %s", group_order.name) + _logger.warning( + "group_order.pickup_day: %s (type: %s)", + group_order.pickup_day, + type(group_order.pickup_day), ) + _logger.warning( + "group_order.pickup_date: %s (type: %s)", + group_order.pickup_date, + type(group_order.pickup_date), + ) + _logger.warning( + "group_order.delivery_date: %s (type: %s)", + group_order.delivery_date, + type(group_order.delivery_date), + ) + _logger.warning("group_order.home_delivery: %s", group_order.home_delivery) + _logger.warning("group_order.delivery_notice: %s", group_order.delivery_notice) + if group_order.pickup_date: + _logger.warning( + "pickup_date formatted: %s", + group_order.pickup_date.strftime("%d/%m/%Y"), + ) + _logger.warning("========================================") # Get delivery product from group_order (configured per group order) delivery_product = group_order.delivery_product_id @@ -1296,24 +1278,38 @@ class AplicoopWebsiteSale(WebsiteSale): labels_json = json.dumps(labels, ensure_ascii=False) # Online payment turns the primary button from "save a draft" into - # the payment form itself. Everything the template needs is resolved - # here so it only reads attributes. + # "confirm and pay". Everything the template needs is resolved here so + # it only reads attributes. online_payment = bool(group_order.online_payment) payment_available = online_payment and self._has_available_payment_method( group_order ) - checkout_button = { - "label": labels.get("save_draft", "Save Draft"), - "hint": labels.get("save_order_as_draft", "Save order as draft"), - "done_label": labels.get("order_saved_as_draft", "Order saved as draft"), - "icon": "fa-save", - "tooltip_key": "save_draft", - } + if online_payment: + checkout_button = { + "label": labels.get("confirm_and_pay", "Confirm and pay"), + "hint": labels.get( + "confirm_and_pay_hint", "Confirm the order and go to payment" + ), + "done_label": labels.get( + "order_ready_for_payment", "Order ready for payment" + ), + "icon": "fa-credit-card", + "tooltip_key": "confirm_and_pay", + } + else: + checkout_button = { + "label": labels.get("save_draft", "Save Draft"), + "hint": labels.get("save_order_as_draft", "Save order as draft"), + "done_label": labels.get( + "order_saved_as_draft", "Order saved as draft" + ), + "icon": "fa-save", + "tooltip_key": "save_draft", + } # Prepare template context with explicit debug info template_context = { "group_order": group_order, - "sale_order": order_sudo, "day_names": self._get_day_names(env=request.env), "delivery_product_id": delivery_product_id, "delivery_product_name": delivery_product_name, # Auto-translated to user's language @@ -1332,13 +1328,7 @@ class AplicoopWebsiteSale(WebsiteSale): ), } - if online_payment: - redirect_url, payment_values = self._prepare_checkout_payment_context( - group_order, order_sudo - ) - if redirect_url: - return request.redirect(redirect_url) - template_context.update(payment_values) + _logger.warning("Template context keys: %s", list(template_context.keys())) return request.render( "website_sale_aplicoop.eskaera_checkout", template_context @@ -1346,13 +1336,9 @@ class AplicoopWebsiteSale(WebsiteSale): # === Online payment === - def _eskaera_checkout_url(self, group_order): - """Return the checkout URL of `group_order`. - - Payment happens on the checkout page: there is no separate step, so - the member picks a method next to the very summary they just read. - """ - return self._eskaera_url(group_order, suffix="/checkout") + def _eskaera_payment_url(self, group_order): + """Return the payment step URL of `group_order`.""" + return self._eskaera_url(group_order, suffix="/payment") def _eskaera_payment_confirmation_url(self, group_order, sale_order): """Return the landing URL shown once `sale_order` has been paid.""" @@ -1415,44 +1401,6 @@ class AplicoopWebsiteSale(WebsiteSale): ) return values - def _prepare_checkout_payment_context(self, group_order, order_sudo): - """Resolve the payment half of the checkout page. - - Returns `(redirect_url, values)`: a non-empty `redirect_url` means the - member has nothing left to pay here and the caller must send them - away instead of rendering. Otherwise `values` completes the checkout - context — either the payment form, or the notice of a transaction - already under way. - """ - if not order_sudo: - # Nothing saved for this cycle yet: the page renders its empty - # state and there is no amount to build a payment form around. - return None, {"pending_transaction": False, "payment_ready": False} - - # A transaction already under way must not be duplicated: the first one - # to reach `done` confirms the order, so a second would be an overpay. - last_tx = order_sudo.get_portal_last_transaction() - if last_tx and last_tx.state in ("pending", "authorized", "done"): - if last_tx.state == "done": - return ( - self._eskaera_payment_confirmation_url(group_order, order_sudo), - {}, - ) - return None, {"pending_transaction": last_tx, "payment_ready": False} - - # `_get_payment_values` prices the form as amount_total - amount_paid; - # a non-positive amount has nothing left to charge and would silently - # render the "no payment method" warning. - if order_sudo.currency_id.compare_amounts(order_sudo.amount_total, 0) <= 0: - return ( - self._eskaera_payment_confirmation_url(group_order, order_sudo), - {}, - ) - - values = self._get_eskaera_payment_values(group_order, order_sudo) - values.update({"pending_transaction": False, "payment_ready": True}) - return None, values - @http.route( ["/eskaera//payment"], type="http", @@ -1461,13 +1409,68 @@ class AplicoopWebsiteSale(WebsiteSale): ) @eskaera_route def eskaera_payment(self, group_order_slug, **post): - """Legacy payment step, folded into the checkout page. - - Kept so bookmarks and sessions in flight when the step disappeared - land somewhere sensible rather than on a 404. - """ + """Payment step: pick a method and pay the order placed at checkout.""" group_order = self._get_group_order_by_slug(group_order_slug) - return request.redirect(self._eskaera_checkout_url(group_order)) + if not group_order or not group_order.online_payment: + return request.redirect(self._eskaera_url(group_order, suffix="/checkout")) + + # Entry gate only. Once an order exists the transaction route and the + # landing page stay reachable even if the cycle closes meanwhile, + # otherwise a member who is already at the provider would come back to + # a redirect and their payment would be orphaned. + if group_order.state != "open": + return request.redirect("/eskaera") + + partner = request.env.user.partner_id + try: + self._validate_user_group_access(group_order, request.env.user) + except ValueError: + return request.redirect("/eskaera") + + placed_order = self._find_placed_cycle_order(partner.id, group_order) + if placed_order: + return request.redirect( + self._eskaera_payment_confirmation_url(group_order, placed_order) + ) + + order_sudo = self._find_recent_draft_order(partner.id, group_order) + if not order_sudo: + return request.redirect(self._eskaera_url(group_order, suffix="/checkout")) + + # A transaction already under way must not be duplicated: the first one + # to reach `done` confirms the order, so a second would be an overpay. + last_tx = order_sudo.get_portal_last_transaction() + if last_tx and last_tx.state in ("pending", "authorized", "done"): + if last_tx.state == "done": + return request.redirect( + self._eskaera_payment_confirmation_url(group_order, order_sudo) + ) + return request.render( + "website_sale_aplicoop.eskaera_payment", + { + "group_order": group_order, + "sale_order": order_sudo, + "pending_transaction": last_tx, + }, + ) + + # `_get_payment_values` prices the form as amount_total - amount_paid; + # a non-positive amount has nothing left to charge and would silently + # render the "no payment method" warning. + if order_sudo.currency_id.compare_amounts(order_sudo.amount_total, 0) <= 0: + return request.redirect( + self._eskaera_payment_confirmation_url(group_order, order_sudo) + ) + + values = self._get_eskaera_payment_values(group_order, order_sudo) + values.update( + { + "group_order": group_order, + "sale_order": order_sudo, + "pending_transaction": False, + } + ) + return request.render("website_sale_aplicoop.eskaera_payment", values) @http.route( ["/eskaera//payment/confirmation/"], @@ -1966,12 +1969,12 @@ class AplicoopWebsiteSale(WebsiteSale): "sale_order_id": sale_order.id, "pickup_slot_label": pickup_slot_label, } - # No redirect here. Saving used to bounce the member to a separate - # payment step, which also killed the confirmation notice on the - # way out; payment now lives on the checkout page, and the caller - # decides whether it is staying or moving on. + # With online payment on, saving the cart is only the first half of + # placing the order: the frontend follows this URL to the payment + # step. The server builds it so the client never assembles routes. if group_order.online_payment: response_data["message"] = request.env._("Order ready for payment") + response_data["redirect_url"] = self._eskaera_payment_url(group_order) return request.make_response( json.dumps(response_data), @@ -1989,150 +1992,6 @@ class AplicoopWebsiteSale(WebsiteSale): status=500, ) - @http.route( - ["/eskaera/set-home-delivery"], - type="http", - auth="user", - website=True, - methods=["POST"], - csrf=False, - ) - @eskaera_route - def set_eskaera_home_delivery(self, **post): - """Move the delivery line on and off the cycle draft. - - On the shop the delivery product is just another cart entry, but the - checkout renders the sale.order, and its total is what the payment - form charges. So the toggle has to reach the order itself; the client - only says yes or no. - """ - try: - data = self._decode_json_body() - - order_id = data.get("order_id") - try: - order_id = int(order_id) - except (TypeError, ValueError): - return request.make_response( - json.dumps({"error": "order_id is required"}), - [("Content-Type", "application/json")], - status=400, - ) - - group_order = request.env["group.order"].sudo().browse(order_id).exists() - if not group_order: - return request.make_response( - json.dumps({"error": f"Order {order_id} not found"}), - [("Content-Type", "application/json")], - status=400, - ) - if group_order.state != "open": - return self._build_group_order_unavailable_response(group_order) - - current_user = request.env.user - try: - self._validate_user_group_access(group_order, current_user) - except ValueError as e: - return request.make_response( - json.dumps({"error": str(e)}), - [("Content-Type", "application/json")], - status=403, - ) - - if placed_response := self._build_already_placed_response( - current_user.partner_id.id, group_order - ): - return placed_response - - draft = self._find_recent_draft_order( - current_user.partner_id.id, group_order - ) - if not draft: - return request.make_response( - json.dumps( - { - "error": request.env._( - "No draft orders found for the current order period" - ) - } - ), - [("Content-Type", "application/json")], - status=404, - ) - draft = draft[0].sudo() - - is_delivery = self._to_bool(data.get("is_delivery", False)) - effective_delivery, commitment_date = self._get_effective_delivery_context( - group_order, is_delivery - ) - delivery_product = group_order.delivery_product_id - - existing_lines = draft.order_line.filtered( - lambda line: line.product_id == delivery_product - ) - if effective_delivery and delivery_product: - if not existing_lines: - pricing = self._get_pricing_info( - delivery_product, self._resolve_pricelist(), quantity=1.0 - ) - draft.write( - { - "order_line": [ - ( - 0, - 0, - { - "product_id": delivery_product.id, - "product_uom_qty": 1.0, - "price_unit": pricing.get( - "price_unit", delivery_product.list_price - ), - "name": delivery_product.with_context( - lang=request.env.lang - ).name, - }, - ) - ] - } - ) - elif existing_lines: - existing_lines.unlink() - - draft.write( - { - "home_delivery": effective_delivery, - "commitment_date": commitment_date, - } - ) - - _logger.info( - "set_eskaera_home_delivery: order %s home_delivery=%s", - draft.id, - effective_delivery, - ) - - return request.make_response( - json.dumps( - { - "success": True, - "is_delivery": effective_delivery, - "sale_order_id": draft.id, - "delivery_product_id": ( - delivery_product.id if delivery_product else None - ), - } - ), - [("Content-Type", "application/json")], - ) - - except Exception as e: - _logger.exception("set_eskaera_home_delivery: Unexpected error") - return request.make_response( - json.dumps({"error": str(e)}), - [("Content-Type", "application/json")], - status=500, - ) - @http.route( ["/eskaera/confirm"], type="http", diff --git a/website_sale_aplicoop/readme/CONFIGURE.rst b/website_sale_aplicoop/readme/CONFIGURE.rst index 79a60cb..e39e649 100644 --- a/website_sale_aplicoop/readme/CONFIGURE.rst +++ b/website_sale_aplicoop/readme/CONFIGURE.rst @@ -73,10 +73,9 @@ confirms in bulk. offer it everywhere, or set it to restrict the provider to one site. #. Open the group order form → *Online Payment* tab → tick **Online payment**. -With the flag on, paying is the only way to place an order: the payment -methods appear on ``/eskaera//checkout``, next to the order summary, and -the member's sale order is confirmed as soon as the transaction completes. -There is no separate payment step — the old ``/eskaera//payment`` URL -redirects to the checkout. Members who have not paid by the cutoff +With the flag on, the checkout button becomes "Confirm and pay" and paying is +the only way to place an order: the member goes through +``/eskaera//payment``, picks a method, and their sale order is confirmed +as soon as the transaction completes. Members who have not paid by the cutoff date still get their draft confirmed by the cron, exactly as they do today — the flag decides how orders are placed, not who gets served. diff --git a/website_sale_aplicoop/readme/USAGE.rst b/website_sale_aplicoop/readme/USAGE.rst index 8283ea0..74edb5e 100644 --- a/website_sale_aplicoop/readme/USAGE.rst +++ b/website_sale_aplicoop/readme/USAGE.rst @@ -50,11 +50,9 @@ Paying an order online When the group order has online payment enabled: #. Build the cart in ``/eskaera/`` as usual -#. Press **Proceed to Checkout**: the cart is saved to your order first, so - the checkout shows the very lines and amounts that will be charged -#. Review the summary, choose home delivery if offered, and pick a payment - method — all on the same page -#. Press **Confirm and pay**; you are sent to the provider and back +#. Go to the checkout, review the summary and choose home delivery if offered +#. Press **Confirm and pay**: the order is saved and the payment step opens +#. Pick a payment method and pay; you are sent to the provider and back #. The confirmation page shows the outcome and empties the local cart The order is confirmed the moment the payment goes through, so it can no diff --git a/website_sale_aplicoop/static/src/js/checkout_labels.js b/website_sale_aplicoop/static/src/js/checkout_labels.js new file mode 100644 index 0000000..3971e74 --- /dev/null +++ b/website_sale_aplicoop/static/src/js/checkout_labels.js @@ -0,0 +1,340 @@ +/** + * Checkout Labels Loading + * Fetches translated labels for checkout table summary + * IMPORTANT: This script waits for the cart to be loaded by website_sale.js + * before rendering the checkout summary. + */ + +(function () { + "use strict"; + + console.log("[CHECKOUT] Script loaded"); + + // Get order ID from button + var confirmBtn = document.getElementById("confirm-order-btn"); + if (!confirmBtn) { + console.log("[CHECKOUT] No confirm button found"); + return; + } + + var orderId = confirmBtn.getAttribute("data-order-id"); + if (!orderId) { + console.log("[CHECKOUT] No order ID found"); + return; + } + + console.log("[CHECKOUT] Order ID:", orderId); + + // Get summary div + var summaryDiv = document.getElementById("checkout-summary"); + if (!summaryDiv) { + console.log("[CHECKOUT] No summary div found"); + return; + } + + // Function to fetch labels and render checkout + var fetchLabelsAndRender = function () { + console.log("[CHECKOUT] Fetching labels..."); + + // Wait for window.groupOrderShop.labels to be initialized (contains hardcoded labels) + var waitForLabels = function (callback, maxWait = 3000, checkInterval = 50) { + var startTime = Date.now(); + var checkLabels = function () { + if ( + window.groupOrderShop && + window.groupOrderShop.labels && + Object.keys(window.groupOrderShop.labels).length > 0 + ) { + console.log("[CHECKOUT] ✅ Hardcoded labels found, proceeding"); + callback(); + } else if (Date.now() - startTime < maxWait) { + setTimeout(checkLabels, checkInterval); + } else { + console.log("[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway"); + callback(); + } + }; + checkLabels(); + }; + + waitForLabels(function () { + // Now fetch additional labels from server + // Detect current language from document or navigator + var currentLang = + document.documentElement.lang || + document.documentElement.getAttribute("lang") || + navigator.language || + "es_ES"; + console.log("[CHECKOUT] Detected language:", currentLang); + + fetch("/eskaera/labels", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + lang: currentLang, + }), + }) + .then(function (response) { + console.log("[CHECKOUT] Response status:", response.status); + return response.json(); + }) + .then(function (data) { + console.log("[CHECKOUT] Response data:", data); + var serverLabels = data.result || data; + console.log( + "[CHECKOUT] Server labels count:", + Object.keys(serverLabels).length + ); + console.log("[CHECKOUT] Sample server labels:", { + draft_merged_success: serverLabels.draft_merged_success, + home_delivery: serverLabels.home_delivery, + }); + + // CRITICAL: Merge server labels with existing hardcoded labels + // Hardcoded labels MUST take precedence over server labels + if (window.groupOrderShop && window.groupOrderShop.labels) { + var existingLabels = window.groupOrderShop.labels; + console.log( + "[CHECKOUT] Existing hardcoded labels count:", + Object.keys(existingLabels).length + ); + console.log("[CHECKOUT] Sample existing labels:", { + draft_merged_success: existingLabels.draft_merged_success, + home_delivery: existingLabels.home_delivery, + }); + + // Start with server labels, then overwrite with hardcoded ones + var mergedLabels = Object.assign({}, serverLabels); + Object.assign(mergedLabels, existingLabels); + + window.groupOrderShop.labels = mergedLabels; + console.log( + "[CHECKOUT] ✅ Merged labels - final count:", + Object.keys(mergedLabels).length + ); + console.log("[CHECKOUT] Verification:", { + draft_merged_success: mergedLabels.draft_merged_success, + home_delivery: mergedLabels.home_delivery, + }); + } else { + // If no existing labels, use server labels as fallback + if (window.groupOrderShop) { + window.groupOrderShop.labels = serverLabels; + } + console.log("[CHECKOUT] ⚠️ No existing labels, using server labels"); + } + + window.renderCheckoutSummary(window.groupOrderShop.labels); + }) + .catch(function (error) { + console.error("[CHECKOUT] Error:", error); + // Fallback to translated labels + window.renderCheckoutSummary(window.getCheckoutLabels()); + }); + }); + }; + + // Listen for cart ready event instead of polling + if (window.groupOrderShop && window.groupOrderShop.orderId) { + // Cart already initialized, render immediately + console.log("[CHECKOUT] Cart already ready"); + fetchLabelsAndRender(); + } else { + // Wait for cart initialization event + console.log("[CHECKOUT] Waiting for cart ready event..."); + document.addEventListener( + "groupOrderCartReady", + function () { + console.log("[CHECKOUT] Cart ready event received"); + fetchLabelsAndRender(); + }, + { once: true } + ); + + // Fallback timeout in case event never fires + setTimeout(function () { + if (window.groupOrderShop && window.groupOrderShop.orderId) { + console.log("[CHECKOUT] Fallback timeout triggered"); + fetchLabelsAndRender(); + } + }, 500); + } + + /** + * Render order summary table or empty message + * Exposed globally so other scripts can call it + */ + window.renderCheckoutSummary = function (labels) { + labels = labels || window.getCheckoutLabels(); + + var summaryDiv = document.getElementById("checkout-summary"); + if (!summaryDiv) return; + + var cartKey = + "eskaera_" + + (document.getElementById("confirm-order-btn") + ? document.getElementById("confirm-order-btn").getAttribute("data-order-id") + : "1") + + "_cart"; + var cart = JSON.parse(localStorage.getItem(cartKey) || "{}"); + + var summaryTable = summaryDiv.querySelector(".checkout-summary-table"); + var tbody = summaryDiv.querySelector("#checkout-summary-tbody"); + var totalSection = summaryDiv.querySelector(".checkout-total-section"); + + // If no table found, create it with headers (shouldn't happen, but fallback) + if (!summaryTable) { + var html = + '' + + '" + + '" + + '" + + '" + + '
' + + escapeHtml(labels.product) + + "' + + escapeHtml(labels.quantity) + + "' + + escapeHtml(labels.price) + + "' + + escapeHtml(labels.subtotal) + + "
' + + '
' + + '' + + escapeHtml(labels.total) + + "" + + '€0.00' + + "
"; + summaryDiv.innerHTML = html; + summaryTable = summaryDiv.querySelector(".checkout-summary-table"); + tbody = summaryDiv.querySelector("#checkout-summary-tbody"); + totalSection = summaryDiv.querySelector(".checkout-total-section"); + } + + // Clear only tbody, preserve headers + tbody.innerHTML = ""; + + if (Object.keys(cart).length === 0) { + // Show empty message if cart is empty + var emptyRow = document.createElement("tr"); + emptyRow.id = "checkout-empty-row"; + emptyRow.className = "empty-message"; + emptyRow.innerHTML = + '' + + '' + + "

" + + escapeHtml(labels.empty) + + "

" + + ""; + tbody.appendChild(emptyRow); + + // Hide total section + totalSection.style.display = "none"; + } else { + // Hide empty row if visible + var emptyRow = tbody.querySelector("#checkout-empty-row"); + if (emptyRow) emptyRow.remove(); + + // Get delivery product ID from page data + var checkoutPage = document.querySelector(".eskaera-checkout-page"); + var deliveryProductId = checkoutPage + ? checkoutPage.getAttribute("data-delivery-product-id") + : null; + + // Separate normal products from delivery product + var normalProducts = []; + var deliveryProduct = null; + + Object.keys(cart).forEach(function (productId) { + if (productId === deliveryProductId) { + deliveryProduct = { id: productId, item: cart[productId] }; + } else { + normalProducts.push({ id: productId, item: cart[productId] }); + } + }); + + // Sort normal products numerically + normalProducts.sort(function (a, b) { + return parseInt(a.id) - parseInt(b.id); + }); + + var total = 0; + + // Render normal products first + normalProducts.forEach(function (product) { + var item = product.item; + var qty = parseFloat(item.quantity || item.qty || 1); + if (isNaN(qty)) qty = 1; + var price = parseFloat(item.price || 0); + if (isNaN(price)) price = 0; + var subtotal = qty * price; + total += subtotal; + + var row = document.createElement("tr"); + row.innerHTML = + "" + + escapeHtml(item.name) + + "" + + '' + + qty.toFixed(2).replace(/\.?0+$/, "") + + "" + + '€' + + price.toFixed(2) + + "" + + '€' + + subtotal.toFixed(2) + + ""; + tbody.appendChild(row); + }); + + // Render delivery product last if present + if (deliveryProduct) { + var item = deliveryProduct.item; + var qty = parseFloat(item.quantity || item.qty || 1); + if (isNaN(qty)) qty = 1; + var price = parseFloat(item.price || 0); + if (isNaN(price)) price = 0; + var subtotal = qty * price; + total += subtotal; + + var row = document.createElement("tr"); + row.innerHTML = + "" + + escapeHtml(item.name) + + "" + + '' + + qty.toFixed(2).replace(/\.?0+$/, "") + + "" + + '€' + + price.toFixed(2) + + "" + + '€' + + subtotal.toFixed(2) + + ""; + tbody.appendChild(row); + } + + // Update total + var totalAmount = summaryDiv.querySelector("#checkout-total-amount"); + if (totalAmount) { + totalAmount.textContent = "€" + total.toFixed(2); + } + + // Show total section + totalSection.style.display = "block"; + } + + console.log("[CHECKOUT] Summary rendered"); + }; + + /** + * Escape HTML to prevent XSS + */ + function escapeHtml(text) { + var div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } +})(); diff --git a/website_sale_aplicoop/static/src/js/home_delivery.js b/website_sale_aplicoop/static/src/js/home_delivery.js index ccfa8dd..795dc2c 100644 --- a/website_sale_aplicoop/static/src/js/home_delivery.js +++ b/website_sale_aplicoop/static/src/js/home_delivery.js @@ -68,15 +68,10 @@ } } - // Get order ID from multiple possible sources. The checkout page - // carries it on its wrapper: with online payment on there is no - // confirm button there, the payment form takes its place. + // Get order ID from multiple possible sources var confirmBtn = document.getElementById("confirm-order-btn"); var cartContainer = document.getElementById("cart-items-container"); - var orderIdElement = - confirmBtn || - cartContainer || - (checkoutPage && checkoutPage.getAttribute("data-order-id") ? checkoutPage : null); + var orderIdElement = confirmBtn || cartContainer; // The URL is not a fallback here: it carries the slug of the order, // not its id. @@ -86,16 +81,22 @@ console.log("[HomeDelivery] orderId resolved:", this.orderId); - // Handle checkbox (only exists on checkout page). Its state is - // rendered from `sale_order.home_delivery`, so it is not read back - // from localStorage here: the order is what the payment form - // charges, and the two must not disagree. + // Handle checkbox (only exists on checkout page) var checkbox = document.getElementById("home-delivery-checkbox"); if (checkbox) { var self = this; checkbox.addEventListener("change", function () { - self.setDeliveryOnOrder(this.checked, this); + if (this.checked) { + self.addDeliveryProduct(); + self.showDeliveryInfo(); + } else { + self.removeDeliveryProduct(); + self.hideDeliveryInfo(); + } }); + + // Check if delivery product is already in cart on page load + this.checkDeliveryInCart(); } // Vincular botón Home Delivery en el shop SOLO si hay un producto de delivery válido @@ -132,6 +133,11 @@ homeDeliveryBtn.classList.remove("btn-outline-warning"); homeDeliveryBtn.classList.add("active", "btn-warning"); } + + // Trigger cart reload to update UI + if (typeof window.renderCheckoutSummary === "function") { + window.renderCheckoutSummary(); + } }); // Set initial button state @@ -178,92 +184,17 @@ } }, - /** - * Move the delivery line on and off the draft order (checkout page). - * - * The checkout renders the sale.order and its total is what gets - * charged, so the toggle has to reach the order — writing only to - * localStorage would change the summary and leave the amount alone. - * localStorage is kept in step so the shop cart agrees, and the page - * is reloaded to pick up the new total and payment amount. - */ - setDeliveryOnOrder: function (isDelivery, checkbox) { - var self = this; + checkDeliveryInCart: function () { + if (!this.deliveryProductId) return; - if (!this.orderId) { - console.warn("[HomeDelivery] No order id, cannot set delivery"); - return; + var cart = this.getCart(); + if (cart[this.deliveryProductId]) { + var checkbox = document.getElementById("home-delivery-checkbox"); + if (checkbox) { + checkbox.checked = true; + this.showDeliveryInfo(); + } } - - if (checkbox) { - checkbox.disabled = true; - } - - var xhr = new XMLHttpRequest(); - xhr.open("POST", "/eskaera/set-home-delivery", true); - xhr.setRequestHeader("Content-Type", "application/json"); - - xhr.onload = function () { - if (xhr.status !== 200) { - console.error("[HomeDelivery] set-home-delivery failed:", xhr.status); - self.revertCheckbox(checkbox, !isDelivery); - return; - } - var data = {}; - try { - data = JSON.parse(xhr.responseText); - } catch (e) { - data = {}; - } - if (!data.success) { - console.error("[HomeDelivery] set-home-delivery rejected:", data.error); - self.revertCheckbox(checkbox, !isDelivery); - return; - } - - // Keep the shop cart in step with the order before reloading. - var cart = self.getCart(); - if (self.deliveryProductId) { - if (data.is_delivery) { - cart[self.deliveryProductId] = { - id: self.deliveryProductId, - name: self.deliveryProductName, - price: self.deliveryProductPrice, - qty: 1, - }; - } else { - delete cart[self.deliveryProductId]; - } - try { - localStorage.setItem( - "eskaera_" + self.orderId + "_cart", - JSON.stringify(cart) - ); - } catch (e) { - console.warn("[HomeDelivery] Could not update the local cart:", e); - } - } - - window.location.reload(); - }; - - xhr.onerror = function () { - console.error("[HomeDelivery] set-home-delivery connection error"); - self.revertCheckbox(checkbox, !isDelivery); - }; - - xhr.send( - JSON.stringify({ - order_id: self.orderId, - is_delivery: isDelivery, - }) - ); - }, - - revertCheckbox: function (checkbox, previousState) { - if (!checkbox) return; - checkbox.checked = previousState; - checkbox.disabled = false; }, getCart: function () { @@ -285,6 +216,14 @@ window.groupOrderShop._updateCartDisplay(); } } + + // Re-render checkout summary without reloading + setTimeout(function () { + // Use the global function from checkout_labels.js + if (typeof window.renderCheckoutSummary === "function") { + window.renderCheckoutSummary(); + } + }, 50); }, addDeliveryProduct: function () { diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index 6926f18..be8970e 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -20,12 +20,8 @@ // Get order ID first (needed by i18nManager and other functions) var confirmBtn = document.getElementById("confirm-order-btn"); var cartContainer = document.getElementById("cart-items-container"); - // The checkout page carries the id on its wrapper: with online - // payment on there is no confirm button there, the payment form - // takes its place. - this._checkoutPage = document.querySelector(".eskaera-checkout-page[data-order-id]"); - var orderIdElement = confirmBtn || cartContainer || this._checkoutPage; + var orderIdElement = confirmBtn || cartContainer; if (!orderIdElement) { console.log("No elements found to get order ID"); return false; @@ -130,15 +126,6 @@ return; } - // Never on the checkout page. It renders the draft server-side, so - // pulling the same lines back into localStorage would only put the - // two out of step — and it used to resurrect lines the member had - // just deleted in the shop, which then reappeared at payment time. - if (this._checkoutPage) { - console.log("Auto-load draft skipped (checkout renders the order itself)"); - return; - } - // Only auto-load if cart is empty var cartItemsCount = Object.keys(this.cart).length; if (cartItemsCount > 0) { @@ -455,7 +442,8 @@ // Storage layout: // eskaera__cart → items as plain {productId: {...}} // (keeps the contract used by - // home_delivery.js and _saveOrderDraft). + // checkout_labels.js, home_delivery.js + // and _saveOrderDraft). // eskaera__cart_cycle → "YYYY-MM-DD" the cart was saved at. // Eviction only fires when we KNOW the current cycle and it // disagrees with the stored one. If either side is missing @@ -484,8 +472,8 @@ return; } // Migration: v18.0.1.10.0 wrapped the cart as {cutoff_date, items} - // in the same key, which broke home_delivery.js and - // _saveOrderDraft (they iterated Object.keys treating them as + // in the same key, which broke checkout_labels.js / home_delivery.js + // / _saveOrderDraft (they iterated Object.keys treating them as // productIds). Unwrap once; subsequent saves use the canonical // split-key layout below. var items; @@ -759,8 +747,7 @@ }, _getLabels: function () { - // Get current labels from window.groupOrderShop, seeded by the - // labels the page renders inline. + // Get current labels from window.groupOrderShop which is updated by checkout_labels.js console.log("[_getLabels] Starting label resolution..."); console.log("[_getLabels] window.groupOrderShop exists:", !!window.groupOrderShop); @@ -893,7 +880,7 @@ _showConfirmation: function (message, onConfirm, onCancel) { var self = this; - // Get current labels - seeded by the labels the page renders inline + // Get current labels - may be updated by checkout_labels.js endpoint var labels = this._getLabels(); console.log("[_showConfirmation] Using labels:", labels); @@ -1011,15 +998,29 @@ }); } - // The checkout delivery checkbox is no longer restored from - // sessionStorage: it renders from `sale_order.home_delivery`, - // and home_delivery.js writes any change straight back to the - // order. A stale session preference could only contradict it. - - // Send the cart to the server before opening the checkout, so - // the page renders the member's current cart rather than - // whatever the draft happened to hold. - this._attachCheckoutLinkListeners(); + // On checkout page: apply sessionStorage delivery preference to checkbox. + // The shop-page toggle may have stored a "false" preference even though + // the checkbox is checked by default in the template. + var checkoutCheckbox = document.getElementById("home-delivery-checkbox"); + if (checkoutCheckbox) { + var storedDeliveryPref = sessionStorage.getItem( + "eskaera_is_delivery_" + self.orderId + ); + if (storedDeliveryPref !== null) { + checkoutCheckbox.checked = storedDeliveryPref === "true"; + console.log( + "[CHECKOUT] Restored delivery checkbox from sessionStorage:", + checkoutCheckbox.checked + ); + } + // Sync sessionStorage when user manually changes the checkbox + checkoutCheckbox.addEventListener("change", function () { + sessionStorage.setItem( + "eskaera_is_delivery_" + self.orderId, + this.checked ? "true" : "false" + ); + }); + } // Button to reload from draft (in My Cart header - cart pages) var reloadCartBtn = document.getElementById("reload-cart-btn"); @@ -1620,103 +1621,6 @@ self._executeSaveCartAsDraft(items); }, - // Read the cart the way the server wants it. localStorage is the - // source of truth while shopping: home_delivery.js writes to it - // directly, so this.cart can lag behind by a tick. - _collectCartItems: function () { - var cartKey = "eskaera_" + this.orderId + "_cart"; - var storedCart = localStorage.getItem(cartKey); - var cart; - try { - cart = storedCart ? JSON.parse(storedCart) : this.cart; - } catch (e) { - cart = this.cart; - } - return Object.keys(cart || {}).map(function (productId) { - var item = cart[productId]; - return { - product_id: productId, - product_name: item.name, - quantity: item.qty, - product_price: item.price, - }; - }); - }, - - // The checkout renders the draft sale.order, so the cart has to reach - // the server before we navigate. Saving is idempotent: the endpoint - // reuses the cycle's draft and only rewrites its lines when they - // actually differ, so a member who already saved from the shop just - // gets their changes applied. - _attachCheckoutLinkListeners: function () { - var self = this; - var links = document.querySelectorAll(".js-eskaera-checkout"); - console.log("[_attachEventListeners] checkout links found:", links.length); - - links.forEach(function (link) { - link.addEventListener("click", function (e) { - e.preventDefault(); - self._saveCartAndGoToCheckout(link.getAttribute("href")); - }); - }); - }, - - _saveCartAndGoToCheckout: function (checkoutUrl) { - var self = this; - var labels = this._getLabels(); - var items = this._collectCartItems(); - - if (items.length === 0) { - this._showNotification(labels.empty_cart || "Your cart is empty", "warning"); - return; - } - - var orderData = { - order_id: this.orderId, - items: items, - merge_action: "replace", - }; - - // Delivery preference: the shop toggle owns it on this page. - var deliveryBtn = document.getElementById("home-delivery-btn"); - if (deliveryBtn) { - orderData.is_delivery = deliveryBtn.classList.contains("active"); - } - - var xhr = new XMLHttpRequest(); - xhr.open("POST", "/eskaera/save-order", true); - xhr.setRequestHeader("Content-Type", "application/json"); - - xhr.onload = function () { - if (xhr.status === 200) { - window.location.href = checkoutUrl; - return; - } - if (self._isClosedOrderResponse(xhr)) { - self._clearCurrentOrderCartSilently(); - self._updateCartDisplay(); - return; - } - if (self._handleAlreadyPlacedResponse(xhr)) { - return; - } - var message = labels.error_saving_draft || "Error saving cart"; - try { - var errorData = JSON.parse(xhr.responseText); - message = errorData.error || message; - } catch (e) { - message = message + " (HTTP " + xhr.status + ")"; - } - self._showNotification(message, "danger"); - }; - - xhr.onerror = function () { - self._showNotification(labels.connection_error || "Connection error", "danger"); - }; - - xhr.send(JSON.stringify(orderData)); - }, - _executeSaveCartAsDraft: function (items) { var self = this; @@ -2030,10 +1934,13 @@ labels.draft_saved_success || labels.draft_saved || "Order saved as draft successfully"; - // No navigation here: the confirmation notice used - // to be wiped out by an immediate redirect to the - // payment step, so the member saw nothing at all. 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"), diff --git a/website_sale_aplicoop/tests/test_online_payment.py b/website_sale_aplicoop/tests/test_online_payment.py index 9e4403d..d1c53d8 100644 --- a/website_sale_aplicoop/tests/test_online_payment.py +++ b/website_sale_aplicoop/tests/test_online_payment.py @@ -1,7 +1,6 @@ # Copyright 2026 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -import json from datetime import timedelta from types import SimpleNamespace from unittest.mock import patch @@ -472,34 +471,31 @@ class TestOnlinePaymentRoutes(HttpCase): def _slug_url(self, suffix=""): return f"/eskaera/{self.group_order.slug}{suffix}" - def _publish_a_provider(self): - """Make one provider usable, so the checkout renders the payment form.""" - provider = self.env.ref("payment.payment_provider_transfer") - provider.write( - { - "state": "test", - "is_published": True, - "company_id": self.group_order.company_id.id, - } - ) - return provider - - def test_legacy_payment_step_redirects_to_checkout(self): - """The separate payment step is gone; its URL lands on the checkout. - - Kept as a redirect rather than dropped so bookmarks, and sessions - that were mid-flow when the step disappeared, do not hit a 404. - """ + def test_payment_page_renders(self): + """The payment step renders for a member with a draft in the cycle.""" self._create_draft() self.authenticate(self.portal_user.login, self.portal_user.login) + response = self.url_open(self._slug_url("/payment"), allow_redirects=True) + + self.assertEqual(response.status_code, 200) + self.assertIn( + 'data-name="Eskaera Payment"', + response.text, + "The payment step should render its own page, not redirect away", + ) + + def test_payment_page_needs_a_draft(self): + """With nothing in the cart there is nothing to pay for.""" + self.authenticate(self.portal_user.login, self.portal_user.login) + response = self.url_open(self._slug_url("/payment"), allow_redirects=False) self.assertEqual(response.status_code, 303) self.assertTrue(response.headers["Location"].endswith("/checkout")) - def test_legacy_payment_step_redirects_without_online_payment(self): - """Same for a group order that takes no payments at all.""" + def test_payment_page_off_without_online_payment(self): + """The step does not exist for a group order that takes no payments.""" self.group_order.online_payment = False self._create_draft() self.authenticate(self.portal_user.login, self.portal_user.login) @@ -509,69 +505,22 @@ class TestOnlinePaymentRoutes(HttpCase): self.assertEqual(response.status_code, 303) self.assertTrue(response.headers["Location"].endswith("/checkout")) - def test_checkout_renders_the_payment_form(self): - """Picking a payment method happens on the checkout, in one screen.""" - self._publish_a_provider() - self._create_draft() - self.authenticate(self.portal_user.login, self.portal_user.login) + def test_checkout_offers_payment(self): + """The checkout button turns into the 'confirm and pay' variant. - response = self.url_open(self._slug_url("/checkout"), allow_redirects=True) - - self.assertEqual(response.status_code, 200) - self.assertIn( - 'id="payment_method"', - response.text, - "The payment form belongs on the checkout page", - ) - self.assertIn( - 'data-name="Eskaera Checkout"', - response.text, - "There must be no separate payment step to redirect to", - ) - - def test_checkout_summary_comes_from_the_order(self): - """The summary is the order, so it cannot drift from what is charged. - - The cart lives in localStorage while the member shops, and the old - client-rendered summary was a snapshot of it taken at page load: a - draft reloaded afterwards silently added its own lines back, which - only became visible one page later, at payment time. + Asserted on `data-tooltip-key` rather than the label: the website runs + in whatever language the visitor picked, and the label is translated. """ - self._publish_a_provider() - self._create_draft() self.authenticate(self.portal_user.login, self.portal_user.login) response = self.url_open(self._slug_url("/checkout"), allow_redirects=True) self.assertEqual(response.status_code, 200) - self.assertIn(self.product.name, response.text) - self.assertNotIn( - 'id="checkout-summary-tbody"', - response.text, - "The summary must not be the client-rendered table any more", - ) - - def test_checkout_without_a_draft_shows_the_empty_state(self): - """Nothing saved for the cycle: nothing to summarise and nothing to pay.""" - self._publish_a_provider() - self.authenticate(self.portal_user.login, self.portal_user.login) - - response = self.url_open(self._slug_url("/checkout"), allow_redirects=True) - - # Asserted on markup rather than the empty-state wording: the website - # runs in whatever language the visitor picked. - self.assertEqual(response.status_code, 200) - self.assertNotIn( - "checkout-summary-table", - response.text, - "With no order there is nothing to summarise", - ) - self.assertNotIn('id="payment_method"', response.text) + self.assertIn('data-tooltip-key="confirm_and_pay"', response.text) def test_checkout_keeps_save_draft_without_online_payment(self): """With the flag off the checkout is exactly what it was.""" self.group_order.online_payment = False - self._create_draft() self.authenticate(self.portal_user.login, self.portal_user.login) response = self.url_open(self._slug_url("/checkout"), allow_redirects=True) @@ -617,120 +566,3 @@ class TestOnlinePaymentRoutes(HttpCase): self.assertEqual(response.status_code, 303) self.assertIn(f"/payment/confirmation/{order.id}", response.headers["Location"]) - - def _post_json(self, route, payload): - return self.url_open( - route, - data=json.dumps(payload), - headers={"Content-Type": "application/json"}, - ) - - def _save_cart(self, quantity, is_delivery=None): - payload = { - "order_id": self.group_order.id, - "items": [ - { - "product_id": self.product.id, - "product_name": self.product.name, - "quantity": quantity, - "product_price": 10.0, - } - ], - } - if is_delivery is not None: - payload["is_delivery"] = is_delivery - return self._post_json("/eskaera/save-order", payload) - - def _cycle_drafts(self): - return self.env["sale.order"].search( - [ - ("partner_id", "=", self.member_partner.id), - ("group_order_id", "=", self.group_order.id), - ("state", "=", "draft"), - ] - ) - - def test_saving_the_cart_again_updates_the_same_draft(self): - """Pushing the cart reuses the cycle draft rather than adding another. - - The cart reaches the server from the shop's save button and again on - the way into the checkout, so this runs on every ordinary flow. - """ - self.authenticate(self.portal_user.login, self.portal_user.login) - - first = self._save_cart(1) - self.assertEqual(first.status_code, 200) - order_id = first.json()["sale_order_id"] - - second = self._save_cart(3) - self.assertEqual(second.status_code, 200) - self.assertEqual( - second.json()["sale_order_id"], - order_id, - "A second save must update the draft, never create a new one", - ) - - drafts = self._cycle_drafts() - self.assertEqual(len(drafts), 1) - self.assertEqual(drafts.order_line.product_uom_qty, 3) - - def test_saving_an_unchanged_cart_leaves_the_lines_alone(self): - """Nothing changed, nothing rewritten. - - Replacing `order_line` unlinks and recreates every line, so an - idempotent save would churn their ids for no reason. - """ - self.authenticate(self.portal_user.login, self.portal_user.login) - - self._save_cart(2) - line_ids = self._cycle_drafts().order_line.ids - - self._save_cart(2) - - self.assertEqual( - self._cycle_drafts().order_line.ids, - line_ids, - "An unchanged cart must not rewrite the order lines", - ) - - def test_home_delivery_toggle_moves_the_line_on_the_order(self): - """The checkout toggle writes to the order, which is what gets charged.""" - delivery_product = self.env["product.product"].create( - {"name": "Home Delivery", "type": "service", "list_price": 5.0} - ) - self.group_order.delivery_product_id = delivery_product - self.authenticate(self.portal_user.login, self.portal_user.login) - self._save_cart(1) - - response = self._post_json( - "/eskaera/set-home-delivery", - {"order_id": self.group_order.id, "is_delivery": True}, - ) - - self.assertEqual(response.status_code, 200) - self.assertTrue(response.json()["is_delivery"]) - draft = self._cycle_drafts() - self.assertTrue(draft.home_delivery) - self.assertIn(delivery_product, draft.order_line.product_id) - - response = self._post_json( - "/eskaera/set-home-delivery", - {"order_id": self.group_order.id, "is_delivery": False}, - ) - - self.assertEqual(response.status_code, 200) - self.assertFalse(response.json()["is_delivery"]) - draft = self._cycle_drafts() - self.assertFalse(draft.home_delivery) - self.assertNotIn(delivery_product, draft.order_line.product_id) - - def test_home_delivery_toggle_needs_a_draft(self): - """With nothing saved there is no order to put the delivery line on.""" - self.authenticate(self.portal_user.login, self.portal_user.login) - - response = self._post_json( - "/eskaera/set-home-delivery", - {"order_id": self.group_order.id, "is_delivery": True}, - ) - - self.assertEqual(response.status_code, 404) diff --git a/website_sale_aplicoop/tests/test_pricing_delegation.py b/website_sale_aplicoop/tests/test_pricing_delegation.py index 47b067d..1481511 100644 --- a/website_sale_aplicoop/tests/test_pricing_delegation.py +++ b/website_sale_aplicoop/tests/test_pricing_delegation.py @@ -129,67 +129,6 @@ class TestStandardPricelistResolution(PricingDelegationCommon, TransactionCase): self.assertEqual(resolved, expected) -@tagged("post_install", "-at_install") -class TestTaxDisplayFollowsTheWebsite(PricingDelegationCommon, TransactionCase): - """Displayed prices honour the website's `Display Product Prices`. - - Eskaera used to hardcode the tax-included display and ignore the setting. - Now it follows website_sale, which means a website left on the Odoo default - (`tax_excluded`) shows prices without VAT -- correct, but surprising enough - to be worth pinning down here. - """ - - def setUp(self): - super().setUp() - self.pricelist = self._create_pricelist("Display PL") - self.tax_21 = self._create_tax("IVA 21%", 21.0) - self.product = self._create_product("Display Product", 100.0, self.tax_21) - - def _price(self): - return self.controller._get_pricing_info( - self.product, self.pricelist, quantity=1.0 - ) - - def test_gross_price_on_a_tax_included_website(self): - """A consumer-facing website quotes what the member pays.""" - self.website.show_line_subtotals_tax_selection = "tax_included" - - pricing = self._price() - - self.assertAlmostEqual(pricing["price"], 121.0, places=2) - self.assertAlmostEqual(pricing["price_unit"], 100.0, places=2) - self.assertTrue(pricing["tax_included"]) - - def test_net_price_on_a_tax_excluded_website(self): - """A website set to tax-excluded shows the net price.""" - self.website.show_line_subtotals_tax_selection = "tax_excluded" - - pricing = self._price() - - self.assertAlmostEqual(pricing["price"], 100.0, places=2) - self.assertAlmostEqual(pricing["price_unit"], 100.0, places=2) - self.assertFalse(pricing["tax_included"]) - - def test_the_setting_is_what_moves_the_price(self): - """Nothing but the website setting changes between the two.""" - self.website.show_line_subtotals_tax_selection = "tax_included" - gross = self._price()["price"] - - self.website.show_line_subtotals_tax_selection = "tax_excluded" - net = self._price()["price"] - - self.assertAlmostEqual(gross - net, 21.0, places=2) - - def test_batched_listing_follows_the_same_setting(self): - """The listing path must not disagree with the single-product one.""" - self.website.show_line_subtotals_tax_selection = "tax_excluded" - - batched = self.controller._compute_price_info(self.product, self.pricelist) - - self.assertAlmostEqual(batched[self.product.id]["price"], 100.0, places=2) - self.assertFalse(batched[self.product.id]["tax_included"]) - - @tagged("post_install", "-at_install") class TestTaxIncludedFiscalPosition(PricingDelegationCommon, TransactionCase): """A remapped tax-included tax must rebase the displayed price.""" diff --git a/website_sale_aplicoop/tests/test_templates_rendering.py b/website_sale_aplicoop/tests/test_templates_rendering.py index 6cc9cc8..12dfe06 100644 --- a/website_sale_aplicoop/tests/test_templates_rendering.py +++ b/website_sale_aplicoop/tests/test_templates_rendering.py @@ -116,9 +116,9 @@ class TestTemplatesRendering(TransactionCase): # The fix ensures no exists # which was causing the NoneType error - def test_order_lines_summary_template_exists(self): - """The checkout summary sub-template exists and reads the sale.order.""" - template = self.env.ref("website_sale_aplicoop.eskaera_order_lines_summary") + def test_eskaera_checkout_summary_template_exists(self): + """Test that eskaera_checkout_summary sub-template exists.""" + template = self.env.ref("website_sale_aplicoop.eskaera_checkout_summary") self.assertIsNotNone(template) self.assertEqual(template.type, "qweb") # Verify it has the expected structure @@ -127,14 +127,6 @@ class TestTemplatesRendering(TransactionCase): template.arch_db, "Template must have checkout-summary-table id", ) - # The summary is server-side on purpose: what it shows is what the - # payment form charges, so it can never drift from the localStorage - # cart the way the old client-rendered table did. - self.assertIn( - "sale_order.order_line", - template.arch_db, - "Template must read the order lines, not a client-side cart", - ) self.assertIn( "Product", template.arch_db, @@ -145,36 +137,23 @@ class TestTemplatesRendering(TransactionCase): template.arch_db, "Template must have Quantity label for translation", ) + self.assertIn( + "Price", template.arch_db, "Template must have Price label for translation" + ) self.assertIn( "Subtotal", template.arch_db, "Template must have Subtotal label for translation", ) - def test_order_lines_summary_renders(self): - """The summary renders the lines and total of a real order.""" - order = self.env["sale.order"].create( - { - "partner_id": self.supplier.id, - "order_line": [ - ( - 0, - 0, - { - "product_id": self.product.id, - "product_uom_qty": 2, - "price_unit": 5.0, - }, - ) - ], - } - ) - template = self.env.ref("website_sale_aplicoop.eskaera_order_lines_summary") - - html = template._render_template(template.xml_id, {"sale_order": order}) - + def test_eskaera_checkout_summary_renders(self): + """Test that eskaera_checkout_summary renders without errors.""" + template = self.env.ref("website_sale_aplicoop.eskaera_checkout_summary") + # Render the template with empty context + html = template._render_template(template.xml_id, {}) + # Should contain the basic table structure self.assertIn(" -
-
-
-
-

Shop Performance

diff --git a/website_sale_aplicoop/views/website_templates.xml b/website_sale_aplicoop/views/website_templates.xml index e92e9de..dfae385 100644 --- a/website_sale_aplicoop/views/website_templates.xml +++ b/website_sale_aplicoop/views/website_templates.xml @@ -338,10 +338,7 @@