The cart button that opens /eskaera/<slug>/checkout read "Proceed to
Checkout", and the curated eu/ca translations went further ("Ordaintzera
Joan", "Finalitza la Comanda"), all implying a payment step. With
instances running both with and without online payment, revert it to the
neutral "Review Order" in the source string, the JS labels and the es/eu/ca
translations.
Changing the msgid also drops the stale DB translation on update, so the
instance that still showed "Ir al pago" picks up the fresh term.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
868 lines
66 KiB
XML
868 lines
66 KiB
XML
<?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) > 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>
|
||
</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]" />
|
||
 
|
||
<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"> (<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>
|
||
<!-- Paying confirms the order, so no draft is left to
|
||
reuse and nothing else would stop a second, also
|
||
payable, order for the same cycle. -->
|
||
<t t-if="placed_order">
|
||
<div class="alert alert-info d-flex flex-column flex-md-row gap-2 align-items-md-center" role="alert">
|
||
<div class="flex-grow-1">
|
||
<strong>You already placed an order for this cycle.</strong>
|
||
<span t-esc="placed_order.name" />
|
||
</div>
|
||
<a t-att-href="placed_order_url" class="btn btn-sm btn-outline-primary">View my order</a>
|
||
</div>
|
||
</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]" />
|
||
 
|
||
<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"> (<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>
|
||
<!-- One flex row for cart/tags/category/search/products: Bootstrap's
|
||
order utilities need every reordered item to be a sibling of the
|
||
same flex container, so the four blocks that used to live in two
|
||
separate rows (filters above, cart+products below) are now columns
|
||
here. Unprefixed order-* puts the cart first on mobile (right below
|
||
the header, above the filters); order-lg-* restores the desktop
|
||
layout: search+category as one row, then tags, then cart+products. -->
|
||
<div class="row g-2 eskaera-filters" id="realtimeSearch-filters">
|
||
<div class="col-12 col-lg-3 order-1 order-lg-4 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>
|
||
<!-- js-eskaera-checkout: the cart is pushed to the draft
|
||
sale.order before navigating, so the checkout page can
|
||
render (and charge) the very lines the member sees. -->
|
||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success cart-btn-compact js-eskaera-checkout" aria-label="Review Order" title="Review Order" 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 js-eskaera-checkout" title="Review Order" data-bs-toggle="tooltip">
|
||
Review Order
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<t t-if="available_tags">
|
||
<div class="col-12 order-2 order-lg-3">
|
||
<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>
|
||
</t>
|
||
<div class="col-12 col-lg-5 order-3 order-lg-2">
|
||
<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 class="col-12 col-lg-7 order-4 order-lg-1">
|
||
<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-12 col-lg-9 order-5 order-lg-5">
|
||
<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" 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-order-id="{{ group_order.id }}" t-attf-data-delivery-product-id="{{ delivery_product_id }}" t-attf-data-delivery-product-name="{{ delivery_product_name }}" t-attf-data-delivery-product-price="{{ delivery_product_price }}" t-attf-data-home-delivery-enabled="{{ 'true' if group_order.home_delivery else 'false' }}" t-attf-data-pickup-day="{{ group_order.pickup_day }}" t-attf-data-pickup-date="{{ group_order.pickup_date.strftime('%d/%m/%Y') if group_order.pickup_date else '' }}" t-attf-data-next-pickup-slot-label="{{ group_order.next_pickup_slot_id.label if group_order.next_pickup_slot_id else '' }}" t-attf-data-delivery-notice="{{ (group_order.delivery_notice or '').replace(chr(10), ' ').replace(chr(13), ' ') }}">
|
||
<div class="container mt-5">
|
||
<div class="row">
|
||
<div class="col-lg-10 offset-lg-1">
|
||
<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]" />
|
||
 
|
||
<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"> (<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" />
|
||
<!-- The summary comes from the saved sale.order, never from
|
||
the localStorage cart: these are the very lines and
|
||
amounts the payment form is about to charge, so the two
|
||
cannot drift apart. The shop pushes the cart here before
|
||
sending the member over. -->
|
||
<div id="checkout-summary" class="mb-5">
|
||
<t t-if="sale_order">
|
||
<t t-call="website_sale_aplicoop.eskaera_order_lines_summary" />
|
||
</t>
|
||
<t t-else="">
|
||
<div class="alert alert-info" role="status">
|
||
<i class="fa fa-inbox me-2" aria-hidden="true" t-translation="off" />
|
||
<span>This order's cart is empty</span>
|
||
</div>
|
||
</t>
|
||
</div>
|
||
<div class="oe_structure oe_empty mb-4" data-name="After Summary" />
|
||
<t t-if="sale_order">
|
||
<t t-if="group_order.home_delivery">
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-body">
|
||
<div class="form-check eskaera-delivery-check">
|
||
<input type="checkbox" class="form-check-input" id="home-delivery-checkbox" name="home_delivery" t-att-checked="sale_order.home_delivery or None" />
|
||
<label class="form-check-label fw-bold" for="home-delivery-checkbox">Home Delivery</label>
|
||
</div>
|
||
<div t-attf-class="alert alert-info mt-3 eskaera-delivery-notice{{ '' if sale_order.home_delivery else ' d-none' }}" id="delivery-info-alert">
|
||
<p class="mb-2">
|
||
<i class="fa fa-truck" aria-hidden="true" t-translation="off" />
|
||
<t t-if="group_order.delivery_date">
|
||
<strong>Delivery Information:</strong> Your order will be delivered at
|
||
<t t-esc="day_names[(int(group_order.pickup_day) + 1) % 7]" />
|
||
<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />
|
||
<t t-if="group_order.delivery_notice">
|
||
<br />
|
||
<t t-esc="group_order.delivery_notice" />
|
||
</t>
|
||
</t>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</t>
|
||
<t t-if="online_payment">
|
||
<t t-if="pending_transaction">
|
||
<div class="alert alert-info" role="alert">
|
||
<h5 class="alert-heading">
|
||
<i class="fa fa-clock-o me-2" aria-hidden="true" t-translation="off" />
|
||
<span>Payment in progress</span>
|
||
</h5>
|
||
<p class="mb-1">We are still waiting for your payment to be confirmed. Please do not pay again.</p>
|
||
<p class="mb-0">
|
||
<span>Reference</span>:
|
||
<span t-esc="pending_transaction.reference" />
|
||
</p>
|
||
</div>
|
||
<div class="checkout-actions d-grid gap-3">
|
||
<a t-att-href="sale_order.get_portal_url()" class="btn btn-outline-secondary btn-lg">
|
||
<i class="fa fa-file-text-o" aria-hidden="true" t-translation="off" />
|
||
<span>View my order</span>
|
||
</a>
|
||
</div>
|
||
</t>
|
||
<t t-elif="payment_ready and payment_available">
|
||
<h4 class="summary-heading mb-3">Payment Method</h4>
|
||
<!-- The whole provider UI is payment.form; this addon
|
||
configures no provider of its own. -->
|
||
<div id="payment_method" class="o_not_editable mb-4">
|
||
<t t-call="payment.form" />
|
||
</div>
|
||
<!-- Deliberately outside #o_payment_form: website_sale's
|
||
payment_form.js binds every
|
||
[name="o_payment_submit_button"] in the document, on
|
||
top of payment_form.js's own delegated handler inside
|
||
the form. A button inside would get both listeners and
|
||
one click would open two transactions. -->
|
||
<div class="checkout-actions d-grid gap-3">
|
||
<t t-call="payment.submit_button" />
|
||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||
<span>Back to Cart</span>
|
||
</a>
|
||
</div>
|
||
</t>
|
||
<t t-else="">
|
||
<div class="alert alert-warning" role="alert" t-esc="no_payment_method_message" />
|
||
<div class="checkout-actions d-grid gap-3">
|
||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||
<span>Back to Cart</span>
|
||
</a>
|
||
</div>
|
||
</t>
|
||
</t>
|
||
<t t-else="">
|
||
<!-- No online payment: the draft is confirmed in bulk by
|
||
the cutoff cron, so the button only re-saves the cart. -->
|
||
<div class="checkout-actions d-grid gap-3" id="checkout-form-labels">
|
||
<button class="btn btn-success btn-lg" id="confirm-order-btn" t-attf-data-order-id="{{ group_order.id }}" t-att-data-confirmed-label="checkout_button['done_label']" t-att-data-pickup-label="labels.get('pickup_day_label', 'Pickup Day')" t-att-aria-label="checkout_button['hint']" t-att-data-bs-title="checkout_button['label']" t-att-data-tooltip-key="checkout_button['tooltip_key']" data-bs-toggle="tooltip">
|
||
<i t-attf-class="fa {{ checkout_button['icon'] }}" aria-hidden="true" t-translation="off" />
|
||
<span t-esc="checkout_button['label']" />
|
||
</button>
|
||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||
<span>Back to Cart</span>
|
||
</a>
|
||
</div>
|
||
</t>
|
||
</t>
|
||
<t t-else="">
|
||
<div class="checkout-actions d-grid gap-3">
|
||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-success btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||
<span>Back to Cart</span>
|
||
</a>
|
||
</div>
|
||
</t>
|
||
</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>
|
||
</div>
|
||
</t>
|
||
</template>
|
||
<template id="eskaera_order_lines_summary" name="Placed Order Summary">
|
||
<!-- The order summary of every page past the shop. The cart is a
|
||
localStorage object while the member shops, but from the
|
||
checkout on the amounts shown must be the ones that will be
|
||
charged, so they come from the sale.order. -->
|
||
<div class="checkout-summary-container" role="region" tabindex="0" aria-label="Order summary">
|
||
<table class="table table-hover checkout-summary-table">
|
||
<caption class="visually-hidden">Products in this order, with quantity, price and subtotal</caption>
|
||
<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-subtotal text-end">Subtotal</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr t-foreach="sale_order.order_line" t-as="line">
|
||
<td class="col-name" t-esc="line.name" />
|
||
<td class="col-qty text-center" t-esc="line.product_uom_qty" />
|
||
<td class="col-subtotal text-end">
|
||
<span t-field="line.price_total" t-options="{'widget': 'monetary', 'display_currency': sale_order.currency_id}" />
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="checkout-total-section">
|
||
<div class="total-row">
|
||
<span class="total-label">Total</span>:
|
||
<span class="total-amount">
|
||
<span t-field="sale_order.amount_total" t-options="{'widget': 'monetary', 'display_currency': sale_order.currency_id}" />
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template id="eskaera_payment_confirmation" name="Eskaera Payment Confirmation">
|
||
<t t-call="website.layout">
|
||
<div id="wrap" class="eskaera-checkout-page oe_structure oe_empty" data-name="Eskaera Payment Confirmation" t-attf-data-clear-cart-order-id="{{ group_order.id }}">
|
||
<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">Order Confirmed: <t t-esc="group_order.name" />
|
||
</t>
|
||
</t>
|
||
</div>
|
||
<!-- The browser lands here from /payment/status once
|
||
post-processing has run, but that is asynchronous, so
|
||
the page reports the order's actual state instead of
|
||
assuming it was confirmed. -->
|
||
<t t-if="is_paid">
|
||
<div class="alert alert-success" role="alert">
|
||
<h5 class="alert-heading">
|
||
<i class="fa fa-check-circle me-2" aria-hidden="true" t-translation="off" />
|
||
<span>Thank you, your order is confirmed</span>
|
||
</h5>
|
||
<p class="mb-0">
|
||
<span>Order</span>
|
||
<span t-esc="sale_order.name" />
|
||
</p>
|
||
</div>
|
||
</t>
|
||
<t t-else="">
|
||
<div class="alert alert-info" role="alert">
|
||
<h5 class="alert-heading">
|
||
<i class="fa fa-clock-o me-2" aria-hidden="true" t-translation="off" />
|
||
<span>Your payment is being processed</span>
|
||
</h5>
|
||
<p class="mb-0">Your order will be confirmed as soon as we receive the payment. You can follow it from your orders page.</p>
|
||
</div>
|
||
</t>
|
||
<t t-if="sale_order.pickup_slot_label">
|
||
<div class="order-info-card card border-0 shadow-sm mb-4">
|
||
<div class="card-body">
|
||
<dl class="info-pair mb-0">
|
||
<dt class="info-label">Pickup</dt>
|
||
<dd class="info-value" t-esc="sale_order.pickup_slot_label" />
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</t>
|
||
<h4 class="summary-heading mb-3">Order Summary</h4>
|
||
<div class="mb-5">
|
||
<t t-call="website_sale_aplicoop.eskaera_order_lines_summary" />
|
||
</div>
|
||
<div class="checkout-actions d-grid gap-3">
|
||
<a t-att-href="sale_order.get_portal_url()" class="btn btn-primary btn-lg">
|
||
<i class="fa fa-file-text-o" aria-hidden="true" t-translation="off" />
|
||
<span>View my order</span>
|
||
</a>
|
||
<a href="/eskaera" class="btn btn-outline-secondary btn-lg">
|
||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||
<span>Back to Orders</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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">
|
||
<t t-set="has_tags" t-value="bool(product.product_tag_ids)" />
|
||
<t t-set="supplier_text" t-value="product_supplier_info.get(product.id)" />
|
||
<t t-set="has_origin" t-value="bool(product.origin_text)" />
|
||
<t t-set="has_info" t-value="has_tags or bool(supplier_text) or has_origin" />
|
||
<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">
|
||
<div class="product-media">
|
||
<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="has_info">
|
||
<!-- Origin/supplier/tags used to sit unconditionally in the card
|
||
body; on the compact "cromo" tile there is no room to show
|
||
them inline, so they move behind this toggle (see
|
||
.product-info-panel below). -->
|
||
<button type="button" class="product-info-toggle" aria-expanded="false" t-attf-aria-controls="product-info-{{ product.id }}" aria-label="Show origin, supplier and tags" title="Origin, supplier and tags">
|
||
<i class="fa fa-info" aria-hidden="true" t-translation="off" />
|
||
</button>
|
||
</t>
|
||
</div>
|
||
<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" />
|
||
<t t-if="has_info">
|
||
<div t-attf-id="product-info-{{ product.id }}" class="product-info-panel" hidden="hidden">
|
||
<t t-if="has_tags">
|
||
<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-if="supplier_text">
|
||
<p class="product-supplier">
|
||
<small t-esc="supplier_text" />
|
||
</p>
|
||
</t>
|
||
<t t-if="has_origin">
|
||
<p class="product-origin">
|
||
<small>
|
||
<i class="fa fa-map-marker" aria-hidden="true" />
|
||
<t t-out="product.origin_text" />
|
||
</small>
|
||
</p>
|
||
</t>
|
||
</div>
|
||
</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>
|