addons-cm/website_sale_aplicoop/views/website_templates.xml
GitHub Copilot a67181ab42 [IMP] website_sale_aplicoop: skip the product-card tags row when unused
The tags row was reserved (empty, aria-hidden) on every product card so
prices stay aligned across a grid row regardless of which cards have tags.
But when no product in the current batch has a published tag, that row was
still a fixed, permanent gap on every card for a feature nobody was using.

any_product_has_tags is now computed once per batch (controller-side, per
CLAUDE.md's no-logic-in-QWeb rule) from the already-filtered published_tags,
and the template skips the row entirely when it's False across all three
render paths (initial page, load-more, infinite-scroll AJAX).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:50:32 +02:00

780 lines
57 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data>
<template id="eskaera_page" name="Eskaera Page">
<t t-call="website.layout">
<div id="wrap" class="eskaera-page oe_structure oe_empty" data-name="Eskaera Orders">
<div class="container">
<div class="row">
<div class="col-lg-12">
<h1>Available Orders</h1>
<p class="text-muted" role="status">Browse and select an order to view its products.</p>
</div>
</div>
<div class="row mt-4">
<div class="col-lg-12">
<div class="oe_structure oe_empty" data-name="Before Orders" />
<t t-if="active_orders">
<div class="eskaera-orders eskaera-orders-grid">
<t t-foreach="active_orders" t-as="order">
<div class="eskaera-order-card-wrapper">
<div t-attf-class="eskaera-order-card{{ ' eskaera-order-card--special' if order.type == 'special' else '' }}">
<div class="card-body">
<div class="position-absolute order-badge-position">
<span class="badge text-bg-primary d-flex align-items-center gap-2 order-badge-custom">
<i class="fa fa-shopping-bag" aria-hidden="true" />
<strong>
<t t-esc="order.available_products_count" />
</strong>
</span>
</div>
<a t-attf-href="/eskaera/{{ order.slug }}" class="eskaera-order-card-link" t-attf-aria-label="View products for order {{ order.name }}">
<div class="card-header-top d-flex gap-2 align-items-center order-header-margin">
<t t-set="image_to_show" t-value="order.image or (order.group_ids[0].image_1920 if order.group_ids else False)" />
<t t-if="image_to_show">
<img t-att-src="image_data_uri(image_to_show)" alt="Order image" class="order-thumbnail-sm" />
</t>
<div class="flex-grow-1">
<h5 class="card-title mb-1">
<t t-esc="order.name" />
</h5>
<t t-if="order.description">
<p class="text-muted small mb-0 order-desc-text">
<t t-esc="(order.description[:150] + '...') if len(order.description) &gt; 200 else order.description" />
</p>
</t>
</div>
</div>
</a>
<t t-call="website_sale_aplicoop.eskaera_order_card_meta" />
<a t-attf-href="/eskaera/{{ order.slug }}" class="btn btn-primary btn-sm" aria-label="Browse products for {{ order.name }}">
<i class="fa fa-shopping-bag" aria-hidden="true" t-translation="off" />
<span>Browse Products</span>
</a>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-else="">
<div class="eskaera-empty-state">
<div class="alert alert-info" role="status" aria-live="polite">
<p>No group orders available this week.</p>
</div>
</div>
</t>
<div class="oe_structure oe_empty mt-4" data-name="After Orders" />
</div>
</div>
</div>
</div>
<script type="text/javascript">
(function() {
'use strict';
// console.log('[eskaera_page] Loading translated labels for category selector');
// Fetch translated labels from endpoint
fetch('/eskaera/labels', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': odoo.csrf_token || ''
}
})
.then(response =&gt; response.json())
.then(data =&gt; {
var labels = data.result || data;
//console.log('[eskaera_page] Labels received:', labels);
// Update category selector first option text
var categorySelect = document.getElementById('realtime-category-select');
if (categorySelect &amp;&amp; categorySelect.options[0] &amp;&amp; labels &amp;&amp; labels.all_categories) {
categorySelect.options[0].text = labels.all_categories;
// console.log('[eskaera_page] Updated category selector to:', labels.all_categories);
} else {
console.log('[eskaera_page] Could not update category selector');
console.log(' categorySelect:', !!categorySelect);
console.log(' categorySelect.options[0]:', categorySelect ? !!categorySelect.options[0] : false);
console.log(' labels:', !!labels);
console.log(' labels.all_categories:', labels ? labels.all_categories : 'N/A');
}
})
.catch(error =&gt; {
console.error('[eskaera_page] Error fetching labels:', error);
});
})();
</script>
</t>
</template>
<template id="confirm_message_snippet" name="Confirm Message Snippet">
<t>Thank you! Your order has been confirmed.</t>
</template>
<template id="confirm_pickup_label_snippet" name="Confirm Pickup Label Snippet">
<t>Pickup Day</t>
</template>
<template id="eskaera_order_card_meta" name="Order Card Metadata">
<div class="card-meta-compact mt-3">
<div class="meta-grid">
<t t-if="order.cutoff_day and order.cutoff_date">
<div class="meta-item">
<span class="meta-label">Cutoff</span>
<span class="meta-value">
<t t-esc="day_names[int(order.cutoff_day) % 7]" /> <t t-esc="order.cutoff_date.strftime('%d/%m')" />
</span>
</div>
</t>
<t t-if="order.pickup_slot_ids">
<div class="meta-item">
<span class="meta-label">Pickup slots</span>
<span class="meta-value">
<t t-foreach="order.pickup_slot_ids" t-as="slot">
<div class="slot-entry">
<t t-esc="day_names[int(slot.weekday) % 7]" />
&#160;
<t t-esc="('%02d:%02d%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
<t t-if="slot.label">&#160;(<t t-esc="slot.label" />)</t>
</div>
</t>
</span>
</div>
</t>
<t t-elif="order.pickup_day and order.pickup_date">
<div class="meta-item">
<span class="meta-label">Pickup</span>
<span class="meta-value">
<t t-esc="day_names[int(order.pickup_day) % 7]" /> <t t-esc="order.pickup_date.strftime('%d/%m')" />
</span>
</div>
</t>
<t t-if="order.home_delivery">
<!-- Badge first, then the date, centred as a pair. Source order
matches reading order, so no CSS reordering is involved.
No "Delivery" label: the badge already names the thing.
text-bg-* instead of bg-*: Bootstrap picks a contrasting
foreground. A badge's default white on green is 3.13:1. -->
<div class="meta-item meta-item--delivery">
<span class="badge text-bg-success"><i class="fa fa-truck" aria-hidden="true" /> Home Delivery</span>
<t t-if="order.delivery_date">
<span class="meta-value">
<t t-esc="day_names[order.delivery_date.weekday()]" /> <t t-esc="order.delivery_date.strftime('%d/%m')" />
</span>
</t>
</div>
</t>
<t t-if="order.type == 'special'">
<div class="meta-item meta-item--full">
<span class="badge special-order-badge"><i class="fa fa-star" aria-hidden="true" /> Special Order</span>
</div>
<t t-if="order.start_date">
<div class="meta-item">
<span class="meta-label">From</span>
<span class="meta-value"><t t-esc="order.start_date.strftime('%d/%m/%Y')" /></span>
</div>
</t>
<t t-if="order.end_date">
<div class="meta-item">
<span class="meta-label">Until</span>
<span class="meta-value"><t t-esc="order.end_date.strftime('%d/%m/%Y')" /></span>
</div>
</t>
</t>
</div>
</div>
</template>
<template id="order_header" name="Order Header">
<div t-att-class="header_class or 'eskaera-order-header'">
<!-- Responsive Bootstrap utilities rather than a CSS override: the
spacing/alignment utilities are !important, so fighting them from
a stylesheet would need !important too. -->
<div class="d-flex flex-column flex-md-row gap-3 gap-md-5 align-items-start align-items-md-center mb-4">
<t t-set="image_to_show" t-value="group_order.image or (group_order.group_ids[0].image_1920 if group_order.group_ids else False)" />
<t t-if="image_to_show">
<img t-att-src="image_data_uri(image_to_show)" alt="Order image" class="order-thumbnail-md" />
</t>
<div class="flex-grow-1">
<h1 class="mb-2">
<t t-esc="header_title or group_order.name" />
</h1>
<t t-if="group_order.description">
<p class="text-muted mb-0 order-desc-full">
<t t-esc="group_order.description" />
</p>
</t>
</div>
</div>
</div>
</template>
<template id="eskaera_shop" name="Eskaera Shop">
<t t-call="website.layout">
<div id="wrap" class="eskaera-shop-page oe_structure oe_empty" data-name="Eskaera Shop" t-attf-data-delivery-product-id="{{ delivery_product_id or '' }}" t-attf-data-delivery-product-name="{{ delivery_product_name or '' }}" t-attf-data-delivery-product-price="{{ delivery_product_price or '' }}" t-attf-data-home-delivery-enabled="{{ 'true' if group_order.home_delivery else 'false' }}">
<div class="container">
<div class="row mb-4">
<div class="col-lg-12">
<t t-call="website_sale_aplicoop.order_header">
<t t-set="header_class" t-value="'eskaera-order-header'" />
</t>
<div class="eskaera-order-header">
<!-- Name/value pairs: a description list, so assistive tech
announces each value with its label. The 4 → 2 → 1 column
layout lives in layout/header.css. -->
<dl class="order-info-grid">
<dt class="info-label">Consumer Groups</dt>
<dd class="info-value">
<t t-esc="', '.join(group_order.group_ids.mapped('name'))" />
</dd>
<t t-if="group_order.cutoff_day">
<dt class="info-label">Cutoff Day</dt>
<dd class="info-value">
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" /> (<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</dd>
</t>
<t t-if="group_order.pickup_slot_ids">
<dt class="info-label">Store Pickup Slots</dt>
<dd class="info-value">
<t t-foreach="group_order.pickup_slot_ids" t-as="slot">
<div class="info-value-line">
<t t-esc="day_names[int(slot.weekday) % 7]" />
&#160;
<t t-esc="('%02d:%02d%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
<t t-if="slot.label">&#160;(<t t-esc="slot.label" />)</t>
</div>
</t>
</dd>
</t>
<t t-elif="group_order.pickup_day">
<dt class="info-label">Store Pickup Day</dt>
<dd class="info-value">
<t t-esc="day_names[int(group_order.pickup_day) % 7]" /> (<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)
</dd>
</t>
<t t-if="group_order.delivery_date and group_order.home_delivery">
<dt class="info-label">Home Delivery Day</dt>
<dd class="info-value">
<t t-esc="day_names[group_order.delivery_date.weekday()]" /> (<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</dd>
</t>
<t t-if="group_order.start_date">
<dt class="info-label">Open From</dt>
<dd class="info-value">
<t t-esc="group_order.start_date.strftime('%d/%m/%Y')" />
</dd>
</t>
<t t-if="group_order.end_date">
<dt class="info-label">Open Until</dt>
<dd class="info-value">
<t t-esc="group_order.end_date.strftime('%d/%m/%Y')" />
</dd>
</t>
</dl>
</div>
</div>
</div>
<div class="mb-3 eskaera-filters" id="realtimeSearch-filters">
<div class="row g-2">
<div class="col-md-7">
<div class="search-input-wrapper">
<label class="visually-hidden" for="realtime-search-input">Search products</label>
<input type="search" id="realtime-search-input" class="form-control eskaera-search-input" placeholder="Search products..." autocomplete="off" aria-describedby="realtime-search-status" />
<button type="button" id="clear-search-btn" class="search-clear-btn" aria-label="Clear search" title="Clear search">
×
</button>
</div>
<p id="realtime-search-status" class="visually-hidden" aria-live="polite" aria-atomic="true" />
</div>
<div class="col-md-5">
<select name="category" id="realtime-category-select" class="form-select">
<option value="">Browse Product Categories</option>
<t t-call="website_sale_aplicoop.category_hierarchy_options">
<t t-set="categories" t-value="category_hierarchy" />
<t t-set="depth" t-value="0" />
</t>
</select>
</div>
</div>
<t t-if="available_tags">
<div class="row mt-3">
<div class="col-12">
<div id="tag-filter-container" class="tag-filter-badges">
<t t-foreach="available_tags" t-as="tag">
<!-- Toggle buttons: aria-pressed carries the filter
state, which realtime_search.js keeps in sync
with the .is-selected / .is-dimmed classes.
The record colour rides in a custom property so
the stylesheet keeps owning the badge. -->
<t t-if="tag['color']">
<button type="button" class="badge tag-filter-badge" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" t-att-data-tag-color="tag['color']" t-attf-style="--ac-tag-color: {{ tag['color'] }};" data-toggle="tag-filter" aria-pressed="false">
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
</button>
</t>
<t t-else="">
<button type="button" class="badge tag-filter-badge tag-use-theme-color" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" data-tag-color="" data-toggle="tag-filter" aria-pressed="false">
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
</button>
</t>
</t>
</div>
</div>
</div>
</t>
</div>
<div class="row g-2">
<div class="col-lg-3 mb-4 mb-lg-0">
<div class="card sticky-top cart-sticky-position" aria-label="Cart Summary">
<div class="card-header d-flex justify-content-between align-items-center gap-1">
<h6 class="mb-0 cart-title-sm" id="cart-title">My Cart</h6>
<div class="btn-group cart-btn-group gap-0" role="group">
<!-- data-bs-title feeds the tooltip, it is not an
accessible name: this icon-only button had none. -->
<button type="button" class="btn btn-primary cart-btn-compact" id="save-cart-btn" t-attf-data-order-id="{{ group_order.id }}" aria-label="Save Cart" title="Save Cart" data-bs-toggle="tooltip">
<i class="fa fa-save cart-icon-size" aria-hidden="true" />
</button>
<t t-if="group_order.home_delivery and delivery_product_id">
<button type="button" class="btn btn-outline-warning cart-btn-compact" id="home-delivery-btn" t-att-aria-label="labels.get('toggle_home_delivery', 'Toggle home delivery')" t-att-data-bs-title="labels.get('home_delivery', 'Home Delivery')" data-bs-toggle="tooltip">
<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">
<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">
<i class="fa fa-trash cart-icon-size" aria-hidden="true" />
</button>
</div>
</div>
<div class="card-body eskaera-cart-items" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-build-version="{{ module_version }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
<p class="text-muted">This order's cart is empty</p>
</div>
<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 }}" 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">
Proceed to Checkout
</a>
</div>
</div>
</div>
<div class="col-lg-9">
<div class="oe_structure oe_empty" data-name="Before Products Filter" />
<t t-if="products">
<div class="products-grid" id="products-grid">
<t t-call="website_sale_aplicoop.eskaera_shop_products" />
</div>
<div id="eskaera-config" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-search="{{ search_query }}" t-attf-data-category="{{ selected_category }}" t-attf-data-per-page="{{ per_page }}" t-attf-data-current-page="{{ current_page }}" t-attf-data-has-next="{{ 'true' if has_next else 'false' }}" class="d-none">
</div>
<t t-if="lazy_loading_enabled and has_next">
<div id="infinite-scroll-container" class="row mt-4">
<div class="col-12 text-center">
<div id="loading-spinner" class="d-none p-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading more products...</p>
</div>
<button id="load-more-btn" class="btn btn-primary btn-lg d-none" t-attf-data-page="{{ current_page + 1 }}" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-search="{{ search_query }}" t-attf-data-category="{{ selected_category }}" t-attf-data-per-page="{{ per_page }}" aria-label="Load more products">
<i class="fa fa-download me-2" aria-hidden="true" />Load More Products
</button>
</div>
</div>
</t>
</t>
<t t-else="">
<div class="alert alert-warning" role="status" aria-live="polite">
<p>No products available in this order.</p>
</div>
</t>
<div class="oe_structure oe_empty mt-4" data-name="After Products" />
</div>
</div>
</div>
</div>
<script type="text/javascript">
(function() {
'use strict';
function initializeTooltips() {
console.log('[TOOLTIP] Initializing tooltips using native title attribute...');
var tooltipElements = document.querySelectorAll('[data-bs-toggle="tooltip"]');
console.log('[TOOLTIP] Found', tooltipElements.length, 'tooltip elements');
var successCount = 0;
tooltipElements.forEach(function(element) {
// Static labels now live in the title attribute, which QWeb
// translates; data-bs-title (which it does not translate) is
// only left where the text comes from the i18n endpoint.
var title = element.getAttribute('data-bs-title');
if (title) {
element.setAttribute('title', title);
successCount++;
}
});
console.log('[TOOLTIP] Tooltip initialization complete:', successCount, 'elements updated');
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeTooltips);
} else {
// DOM is already loaded
initializeTooltips();
}
})();
</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 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">Confirm Order: <t t-esc="group_order.name" />
</t>
</t>
</div>
<div class="order-info-card card border-0 shadow-sm mb-4">
<div class="card-body">
<!-- Name/value pairs, same treatment as the shop header:
a <label> without a form control to point at labels
nothing, while <dt>/<dd> ties value to name. -->
<div class="row mb-4">
<div class="col-md-4">
<dl class="info-pair">
<dt class="info-label">Cutoff Day</dt>
<t t-if="group_order.cutoff_day and group_order.cutoff_date">
<dd class="info-value">
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" />
<span class="info-date">(<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
</dd>
</t>
<t t-else="">
<dd class="info-value text-muted small">Not configured</dd>
</t>
</dl>
</div>
<div class="col-md-4">
<t t-if="group_order.pickup_slot_ids">
<dl class="info-pair">
<dt class="info-label">Store Pickup Slots</dt>
<dd class="info-value">
<t t-foreach="group_order.pickup_slot_ids" t-as="slot">
<div class="info-value-line">
<t t-esc="day_names[int(slot.weekday) % 7]" />
&#160;
<t t-esc="('%02d:%02d%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
<t t-if="slot.label">&#160;(<t t-esc="slot.label" />)</t>
</div>
</t>
</dd>
</dl>
</t>
<t t-elif="group_order.pickup_day and group_order.pickup_date">
<dl class="info-pair">
<dt class="info-label">Store Pickup Day</dt>
<dd class="info-value" t-attf-data-pickup-date="{{ group_order.pickup_date }}">
<t t-esc="day_names[int(group_order.pickup_day) % 7]" />
<span class="info-date">(<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)</span>
</dd>
</dl>
</t>
</div>
<div class="col-md-4">
<t t-if="group_order.delivery_date and group_order.home_delivery">
<dl class="info-pair">
<dt class="info-label">Home Delivery Day</dt>
<dd class="info-value">
<t t-esc="day_names[group_order.delivery_date.weekday()]" />
<span class="info-date">(<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</span>
</dd>
</dl>
</t>
</div>
</div>
<hr class="my-2" />
</div>
</div>
<h4 class="summary-heading mb-3">Order Summary</h4>
<div class="oe_structure oe_empty mb-3" data-name="Before Summary" />
<div id="checkout-summary" class="mb-5">
<t t-call="website_sale_aplicoop.eskaera_checkout_summary">
<t t-set="labels" t-value="{}" />
</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>
</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>
</div>
</div>
</div>
</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="labels.get('order_saved_as_draft', 'Order saved as draft')" t-att-data-pickup-label="labels.get('pickup_day_label', 'Pickup Day')" t-att-aria-label="labels.get('save_order_as_draft', 'Save order as draft')" t-att-data-bs-title="labels.get('save_draft', 'Save Draft')" data-bs-toggle="tooltip">
<i class="fa fa-save" aria-hidden="true" t-translation="off" />
<span t-esc="labels.get('save_draft', 'Save Draft')" />
</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>
<script type="text/javascript">
(function() {
'use strict';
// Initialize groupOrderShop.labels from server-rendered labels
if (!window.groupOrderShop) {
window.groupOrderShop = {};
}
window.groupOrderShop.labels = <t t-raw="labels_json" />;
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="category_hierarchy_options" name="Category Hierarchy Options">
<t t-foreach="categories" t-as="cat">
<t t-set="padding_px" t-value="depth * 20" />
<t t-set="prefix">
<t t-foreach="range(depth)" t-as="i"></t>
</t>
<option t-att-value="str(cat['id'])" t-attf-style="padding-left: {{ padding_px }}px;">
<t t-esc="prefix" />
<t t-esc="cat['name']" />
</option>
<t t-if="cat['children']">
<t t-call="website_sale_aplicoop.category_hierarchy_options">
<t t-set="categories" t-value="cat['children']" />
<t t-set="depth" t-value="depth + 1" />
</t>
</t>
</t>
</template>
<template id="eskaera_shop_products" name="Eskaera Shop Products">
<t t-foreach="products" t-as="product">
<div class="product-card-wrapper product-card" t-attf-data-product-name="{{ product.name }}" t-attf-data-category-id="{{ product.categ_id.id if product.categ_id else '' }}" t-attf-data-product-tags="{{ ','.join(str(t.id) for t in product.product_tag_ids) if product.product_tag_ids else '' }}">
<div class="card h-100">
<t t-if="product.image_128">
<img t-attf-src="data:image/png;base64,{{ product.image_128.decode() }}" class="card-img-top product-img-cover" t-attf-alt="{{ product.name }}" />
</t>
<t t-else="">
<div class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder">
<i class="fa fa-image fa-3x text-muted" aria-hidden="true" />
</div>
</t>
<t t-if="product.dynamic_ribbon_id">
<t t-set="ribbon" t-value="product.dynamic_ribbon_id" />
<span t-attf-class="o_ribbon {{ ribbon._get_position_class() }} z-1" t-attf-style="color: {{ ribbon.text_color }}; background-color: {{ ribbon.bg_color }};" t-esc="ribbon.name" />
</t>
<t t-set="user_ribbon" t-value="product.sudo().variant_ribbon_id or product.sudo().website_ribbon_id" />
<t t-if="user_ribbon">
<span t-attf-class="o_ribbon {{ user_ribbon._get_position_class() }} z-1" t-attf-style="color: {{ user_ribbon.text_color }}; background-color: {{ user_ribbon.bg_color }};" t-esc="user_ribbon.name" />
</t>
<div class="card-body d-flex flex-column">
<h6 class="card-title" t-esc="product.name" />
<!-- Tags/supplier/origin always occupy their slot (empty and
aria-hidden when the product has no value) so that the price
row below lines up across every card in the grid row,
regardless of which optional fields a given product has. The
tags row itself is only reserved when at least one product in
this batch actually has a tag (any_product_has_tags, computed
controller-side) — otherwise it would add a permanent empty
gap to every card for a feature nobody in the list uses. -->
<t t-if="any_product_has_tags">
<t t-if="product.product_tag_ids">
<div class="product-tags">
<t t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag">
<t t-if="tag.color">
<!-- Per-record colour: passed as a custom property so the
stylesheet keeps owning the badge, no !important needed. -->
<span class="badge badge-km" t-attf-style="--ac-tag-color: {{ tag.color }};" t-esc="tag.name" />
</t>
<t t-else="">
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" />
</t>
</t>
</div>
</t>
<t t-else="">
<div class="product-tags" aria-hidden="true" />
</t>
</t>
<t t-if="product_supplier_info.get(product.id)">
<p class="product-supplier">
<small>
<t t-esc="product_supplier_info[product.id]" />
</small>
</p>
</t>
<t t-else="">
<p class="product-supplier" aria-hidden="true" />
</t>
<t t-if="product.origin_text">
<p class="product-origin">
<small>
<i class="fa fa-map-marker" aria-hidden="true" />
<t t-out="product.origin_text" />
</small>
</p>
</t>
<t t-else="">
<p class="product-origin" aria-hidden="true" />
</t>
<t t-set="price_info" t-value="product_price_info.get(product.id, {})" />
<t t-set="display_price" t-value="product_display_info.get(product.id, {}).get('display_price', 0.0)" />
<t t-set="base_price" t-value="price_info.get('list_price', product.list_price)" />
<h6 class="card-text product-price-display">
<span class="product-price-main">
<t t-esc="'%.2f' % display_price" />
<t t-if="product_display_info.get(product.id, {}).get('price_unit_suffix')">
<span class="ms-1">
<t t-esc="product_display_info.get(product.id, {}).get('price_unit_suffix')" />
</span>
</t>
</span>
<t t-if="price_info.get('has_discounted_price', False)">
<small class="text-muted text-decoration-line-through ms-1">
<t t-esc="'%.2f' % base_price" />
</small>
</t>
</h6>
<t t-set="tax_incl_base_unit_price" t-value="product_display_info.get(product.id, {}).get('base_unit_price', 0.0)" />
<t t-if="tax_incl_base_unit_price and product.base_unit_name">
<p class="product-unit-price text-muted">
<t t-esc="'%.2f' % tax_incl_base_unit_price" /> € / <t t-esc="product.base_unit_name" />
</p>
</t>
</div>
<t t-set="safe_uom_category" t-value="product_display_info.get(product.id, {}).get('safe_uom_category', '')" />
<t t-set="quantity_step" t-value="product_display_info.get(product.id, {}).get('quantity_step', 1)" />
<t t-set="order_id_safe" t-value="group_order.id if group_order else ''" />
<t t-set="out_of_stock_label" t-value="product_display_info.get(product.id, {}).get('out_of_stock_label', 'Out of stock')" />
<t t-set="add_to_cart_label" t-value="product_display_info.get(product.id, {}).get('add_to_cart_label', 'Add to cart')" />
<form
class="add-to-cart-form"
t-attf-data-order-id="{{ order_id_safe }}"
t-attf-data-product-id="{{ product.id }}"
t-attf-data-product-name="{{ product.name }}"
t-attf-data-product-price="{{ display_price }}"
t-attf-data-uom-category="{{ safe_uom_category }}"
t-attf-data-quantity-step="{{ quantity_step }}"
t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'"
>
<div class="qty-control">
<label t-attf-for="qty_{{ product.id }}" class="visually-hidden">Quantity of <t t-esc="product.name" />
</label>
<button class="qty-decrease" type="button" t-attf-data-product-id="{{ product.id }}" aria-label="Decrease quantity">
<i class="fa fa-minus" aria-hidden="true" />
</button>
<input type="number" t-attf-id="qty_{{ product.id }}" class="product-qty" name="quantity" t-attf-value="1" t-attf-min="{{ quantity_step }}" t-attf-step="{{ quantity_step }}" t-att-max="product_max_qty.get(product.id) if product_max_qty else None" t-att-data-max-qty="product_max_qty.get(product.id) if product_max_qty else None" />
<button class="qty-increase" type="button" t-attf-data-product-id="{{ product.id }}" aria-label="Increase quantity">
<i class="fa fa-plus" aria-hidden="true" />
</button>
<!-- Out of stock stays focusable and clickable on purpose: the handler
explains why instead of the button silently doing nothing.
aria-disabled announces the state without removing it from the
tab order (a real `disabled` would suppress the click event). -->
<button
class="add-to-cart-btn"
type="button"
t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'"
t-att-aria-disabled="'true' if product.is_out_of_stock else None"
t-att-aria-label="out_of_stock_label if product.is_out_of_stock else add_to_cart_label"
t-att-title="out_of_stock_label if product.is_out_of_stock else add_to_cart_label"
>
<i t-attf-class="fa {{ 'fa-ban text-muted' if product.is_out_of_stock else 'fa-shopping-cart' }}" aria-hidden="true" />
</button>
</div>
</form>
</div>
</div>
</t>
</template>
</data>
</odoo>