[FIX] website_sale_aplicoop: Remove redundant string= attributes and fix OCA linting warnings

- Remove redundant string= from 17 field definitions where name matches string value (W8113)
- Convert @staticmethod to instance methods in selection methods for proper self.env._() access
- Fix W8161 (prefer-env-translation) by using self.env._() instead of standalone _()
- Fix W8301/W8115 (translation-not-lazy) by proper placement of % interpolation outside self.env._()
- Remove unused imports of odoo._ from group_order.py and sale_order_extension.py
- All OCA linting warnings in website_sale_aplicoop main models are now resolved

Changes:
- website_sale_aplicoop/models/group_order.py: 21 field definitions cleaned
- website_sale_aplicoop/models/sale_order_extension.py: 5 field definitions cleaned + @staticmethod conversion
- Consistent with OCA standards for addon submission
This commit is contained in:
snt 2026-02-18 17:54:43 +01:00
parent 5c89795e30
commit 6fbc7b9456
73 changed files with 5386 additions and 4354 deletions

View file

@ -1,8 +1,8 @@
# CSS Architecture - Website Sale Aplicoop
**Refactoring Date**: 7 de febrero de 2026
**Status**: ✅ Complete
**Previous Size**: 2,986 líneas en 1 archivo
**Refactoring Date**: 7 de febrero de 2026
**Status**: ✅ Complete
**Previous Size**: 2,986 líneas en 1 archivo
**New Size**: ~400 líneas distribuidas en 15 archivos modulares
---
@ -59,43 +59,43 @@ website_sale_aplicoop/static/src/css/
## 📊 Desglose de Archivos
### **base/** - Fundamentos
- **variables.css** (~80 líneas)
- **variables.css** (~80 líneas)
Colores, tipografía, espaciados, sombras, transiciones, z-index
- **utilities.css** (~15 líneas)
- **utilities.css** (~15 líneas)
Clases utilitarias reutilizables (.sr-only, .text-muted, etc)
### **layout/** - Estructura Global
- **pages.css** (~70 líneas)
- **pages.css** (~70 líneas)
Fondos de página, gradientes, pseudo-elementos (::before)
- **header.css** (~100 líneas)
- **header.css** (~100 líneas)
Headers, navegación, títulos, información de pedidos
- **responsive.css** (~200 líneas)
- **responsive.css** (~200 líneas)
Todas las media queries (4 breakpoints: 992px, 768px, 576px, etc)
### **components/** - Elementos Reutilizables
- **product-card.css** (~80 líneas)
- **product-card.css** (~80 líneas)
Tarjetas de producto con hover, imagen, título, precio
- **order-card.css** (~100 líneas)
- **order-card.css** (~100 líneas)
Tarjetas de orden (Eskaera) con metadatos, badges
- **cart.css** (~150 líneas)
- **cart.css** (~150 líneas)
Carrito lateral, items, total, botones save/reload
- **buttons.css** (~80 líneas)
- **buttons.css** (~80 líneas)
Botones primarios, checkout, acciones
- **quantity-control.css** (~100 líneas)
- **quantity-control.css** (~100 líneas)
Control de cantidad (spinners + input numérico)
- **forms.css** (~70 líneas)
- **forms.css** (~70 líneas)
Inputs, selects, checkboxes, labels
- **alerts.css** (~50 líneas)
- **alerts.css** (~50 líneas)
Alertas, notificaciones, toasts
### **sections/** - Layouts Específicos de Página
- **products-grid.css** (~25 líneas)
- **products-grid.css** (~25 líneas)
Grid de productos con responsive
- **order-list.css** (~40 líneas)
- **order-list.css** (~40 líneas)
Lista de órdenes (Eskaera page)
- **checkout.css** (~100 líneas)
- **checkout.css** (~100 líneas)
Tabla de checkout, totales, summary
- **info-cards.css** (~50 líneas)
- **info-cards.css** (~50 líneas)
Tarjetas de información, metadatos
---
@ -183,7 +183,7 @@ Permitiría mejor nesting y variables más poderosas.
## 📈 Cambios Visuales
**NINGUNO** - La refactorización es solo organizacional
**NINGUNO** - La refactorización es solo organizacional
El CSS compilado genera **exactamente el mismo output** que antes.
---
@ -231,6 +231,6 @@ grep -r "components/\|sections/\|base/\|layout/" css/website_sale.css
---
**Mantenido por**: Equipo de Frontend
**Última actualización**: 7 de febrero de 2026
**Mantenido por**: Equipo de Frontend
**Última actualización**: 7 de febrero de 2026
**Licencia**: AGPL-3.0

View file

@ -16,26 +16,27 @@
--info-color: #17a2b8;
--light-color: #f8f9fa;
--dark-color: #2d3748;
/* Text colors */
--text-primary: #1a202c;
--text-secondary: #4a5568;
--text-muted: #6b7280;
/* Border colors */
--border-light: #e2e8f0;
--border-medium: #cbd5e0;
--border-dark: #718096;
/* ========== TYPOGRAPHY ========== */
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
/* ========== SPACING ========== */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
@ -43,23 +44,23 @@
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* ========== BORDER RADIUS ========== */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* ========== SHADOWS ========== */
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.12);
/* ========== TRANSITIONS ========== */
--transition-fast: 200ms ease;
--transition-normal: 320ms cubic-bezier(.2, .9, .2, 1);
--transition-normal: 320ms cubic-bezier(0.2, 0.9, 0.2, 1);
--transition-slow: 500ms ease;
/* ========== Z-INDEX ========== */
--z-dropdown: 1000;
--z-sticky: 1020;

View file

@ -17,7 +17,8 @@
border: 1px solid rgba(90, 103, 216, 0.12);
border-radius: 0.75rem;
box-shadow: 0 6px 18px rgba(28, 37, 80, 0.06);
transition: transform 320ms cubic-bezier(.2, .9, .2, 1), box-shadow 320ms, border-color 320ms, background 320ms;
transition: transform 320ms cubic-bezier(0.2, 0.9, 0.2, 1), box-shadow 320ms, border-color 320ms,
background 320ms;
overflow: hidden;
display: flex;
flex-direction: column;
@ -139,7 +140,7 @@
}
.eskaera-order-card .btn::before {
content: '';
content: "";
position: absolute;
top: 50%;
left: 50%;

View file

@ -51,7 +51,11 @@
}
.product-card:hover .card-body {
background: linear-gradient(135deg, rgba(108, 117, 125, 0.10) 0%, rgba(108, 117, 125, 0.08) 100%);
background: linear-gradient(
135deg,
rgba(108, 117, 125, 0.1) 0%,
rgba(108, 117, 125, 0.08) 100%
);
}
.product-card .card-title {

View file

@ -21,7 +21,7 @@
.add-to-cart-form .input-group {
width: 100%;
gap: 0;
padding: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;

View file

@ -5,7 +5,7 @@
/**
* Tag Filter Badges Component
*
*
* Styles for interactive tag filter badges in the product search/filter bar.
* Badges toggle between secondary (unselected) and primary (selected) states.
*/
@ -65,7 +65,7 @@
.tag-filter-badges {
gap: 0.375rem;
}
.tag-filter-badge {
padding: 0.375rem 0.625rem;
font-size: 0.8125rem;

View file

@ -4,13 +4,15 @@
* Page backgrounds and main layout structures
*/
html, body {
html,
body {
background-color: transparent !important;
background: transparent !important;
}
body.website_published {
background: linear-gradient(135deg,
background: linear-gradient(
135deg,
color-mix(in srgb, var(--primary-color) 30%, white),
color-mix(in srgb, var(--primary-color) 60%, black)
) !important;
@ -32,21 +34,24 @@ body.website_published .eskaera-checkout-page {
.eskaera-page,
.eskaera-generic-page {
background: linear-gradient(180deg,
background: linear-gradient(
180deg,
color-mix(in srgb, var(--primary-color) 10%, white),
color-mix(in srgb, var(--primary-color) 70%, black)
) !important;
}
.eskaera-shop-page {
background: linear-gradient(135deg,
background: linear-gradient(
135deg,
color-mix(in srgb, var(--primary-color) 10%, white),
color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135))
) !important;
}
.eskaera-checkout-page {
background: linear-gradient(-135deg,
background: linear-gradient(
-135deg,
color-mix(in srgb, var(--primary-color) 0%, white),
color-mix(in srgb, var(--primary-color) 60%, black)
) !important;
@ -54,29 +59,54 @@ body.website_published .eskaera-checkout-page {
.eskaera-page::before,
.eskaera-generic-page::before {
background-image:
radial-gradient(circle at 20% 50%, color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 50%),
radial-gradient(circle at 40% 20%, color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%, transparent 50%);
background-image: radial-gradient(
circle at 20% 50%,
color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 80%,
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 20%,
color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%,
transparent 50%
);
}
.eskaera-shop-page::before {
background-image:
radial-gradient(circle at 15% 30%, color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%, transparent 50%),
radial-gradient(circle at 85% 70%, color-mix(in srgb, var(--primary-color) 22%, transparent) 0%, transparent 50%);
background-image: radial-gradient(
circle at 15% 30%,
color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 85% 70%,
color-mix(in srgb, var(--primary-color) 22%, transparent) 0%,
transparent 50%
);
}
.eskaera-checkout-page::before {
background-image:
radial-gradient(circle at 20% 50%, color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 50%);
background-image: radial-gradient(
circle at 20% 50%,
color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 80%,
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%,
transparent 50%
);
}
.eskaera-page::before,
.eskaera-shop-page::before,
.eskaera-generic-page::before,
.eskaera-checkout-page::before {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;

View file

@ -17,20 +17,20 @@
.cart-items {
max-height: 400px;
}
#cart-items-container {
width: 100%;
padding: 0.75rem;
}
.list-group-item {
padding: 0.75rem;
}
.list-group-item h6 {
font-size: 0.95rem;
}
.list-group-item strong {
min-width: 70px;
}
@ -43,7 +43,7 @@
.cart-header h5 {
font-size: 1.25rem;
}
.cart-title-lg {
font-size: 1.25rem;
}
@ -476,13 +476,13 @@
.product-tags {
font-size: 1.1rem !important;
}
/* Scale down quantity input for 6-column layout */
.add-to-cart-form .product-qty {
font-size: 0.85rem;
max-width: 55px;
}
.add-to-cart-form .qty-decrease,
.add-to-cart-form .qty-increase {
font-size: 0.75rem;
@ -495,13 +495,13 @@
.product-tags {
font-size: 1.25rem !important;
}
/* Scale down quantity input for 5-column layout */
.add-to-cart-form .product-qty {
font-size: 0.9rem;
max-width: 60px;
}
.add-to-cart-form .qty-decrease,
.add-to-cart-form .qty-increase {
font-size: 0.8rem;

View file

@ -19,7 +19,7 @@
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
align-items: start;
margin-top: 0.5rem;
margin-top: 0.5rem;
}
.card-meta-compact {

View file

@ -3,7 +3,7 @@
/**
* Website Sale Aplicoop - Main CSS Index File
* This file imports all component stylesheets in the correct order
*
*
* Architecture:
* 1. Base & Variables (colors, spacing, typography)
* 2. Layout & Pages (page backgrounds, containers)
@ -15,36 +15,36 @@
/* ============================================
1. BASE & VARIABLES
============================================ */
@import 'base/variables.css';
@import 'base/utilities.css';
@import "base/variables.css";
@import "base/utilities.css";
/* ============================================
2. LAYOUT & PAGES
============================================ */
@import 'layout/pages.css';
@import 'layout/header.css';
@import "layout/pages.css";
@import "layout/header.css";
/* ============================================
3. COMPONENTS (Reusable UI elements)
============================================ */
@import 'components/product-card.css';
@import 'components/order-card.css';
@import 'components/cart.css';
@import 'components/buttons.css';
@import 'components/quantity-control.css';
@import 'components/forms.css';
@import 'components/alerts.css';
@import 'components/tag-filter.css';
@import "components/product-card.css";
@import "components/order-card.css";
@import "components/cart.css";
@import "components/buttons.css";
@import "components/quantity-control.css";
@import "components/forms.css";
@import "components/alerts.css";
@import "components/tag-filter.css";
/* ============================================
4. SECTIONS (Page-specific layouts)
============================================ */
@import 'sections/products-grid.css';
@import 'sections/order-list.css';
@import 'sections/checkout.css';
@import 'sections/info-cards.css';
@import "sections/products-grid.css";
@import "sections/order-list.css";
@import "sections/checkout.css";
@import "sections/info-cards.css";
/* ============================================
5. RESPONSIVE DESIGN (Media queries)
============================================ */
@import 'layout/responsive.css';
@import "layout/responsive.css";

View file

@ -5,140 +5,158 @@
* before rendering the checkout summary.
*/
(function() {
'use strict';
(function () {
"use strict";
console.log('[CHECKOUT] Script loaded');
console.log("[CHECKOUT] Script loaded");
// Get order ID from button
var confirmBtn = document.getElementById('confirm-order-btn');
var confirmBtn = document.getElementById("confirm-order-btn");
if (!confirmBtn) {
console.log('[CHECKOUT] No confirm button found');
console.log("[CHECKOUT] No confirm button found");
return;
}
var orderId = confirmBtn.getAttribute('data-order-id');
var orderId = confirmBtn.getAttribute("data-order-id");
if (!orderId) {
console.log('[CHECKOUT] No order ID found');
console.log("[CHECKOUT] No order ID found");
return;
}
console.log('[CHECKOUT] Order ID:', orderId);
console.log("[CHECKOUT] Order ID:", orderId);
// Get summary div
var summaryDiv = document.getElementById('checkout-summary');
var summaryDiv = document.getElementById("checkout-summary");
if (!summaryDiv) {
console.log('[CHECKOUT] No summary div found');
console.log("[CHECKOUT] No summary div found");
return;
}
// Function to fetch labels and render checkout
var fetchLabelsAndRender = function() {
console.log('[CHECKOUT] Fetching labels...');
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 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');
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');
console.log("[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway");
callback();
}
};
checkLabels();
};
waitForLabels(function() {
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',
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',
"Content-Type": "application/json",
},
body: JSON.stringify({
lang: currentLang
lang: currentLang,
}),
})
.then(function (response) {
console.log("[CHECKOUT] Response status:", response.status);
return response.json();
})
})
.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
.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,
});
// 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;
// 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");
}
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());
});
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');
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 });
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() {
setTimeout(function () {
if (window.groupOrderShop && window.groupOrderShop.orderId) {
console.log('[CHECKOUT] Fallback timeout triggered');
console.log("[CHECKOUT] Fallback timeout triggered");
fetchLabelsAndRender();
}
}, 500);
@ -148,67 +166,88 @@
* Render order summary table or empty message
* Exposed globally so other scripts can call it
*/
window.renderCheckoutSummary = function(labels) {
window.renderCheckoutSummary = function (labels) {
labels = labels || window.getCheckoutLabels();
var summaryDiv = document.getElementById('checkout-summary');
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 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');
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 = '<table class="table table-hover checkout-summary-table" id="checkout-summary-table" role="grid" aria-label="Purchase summary"><thead class="table-dark"><tr>' +
'<th scope="col" class="col-name">' + escapeHtml(labels.product) + '</th>' +
'<th scope="col" class="col-qty text-center">' + escapeHtml(labels.quantity) + '</th>' +
'<th scope="col" class="col-price text-right">' + escapeHtml(labels.price) + '</th>' +
'<th scope="col" class="col-subtotal text-right">' + escapeHtml(labels.subtotal) + '</th>' +
var html =
'<table class="table table-hover checkout-summary-table" id="checkout-summary-table" role="grid" aria-label="Purchase summary"><thead class="table-dark"><tr>' +
'<th scope="col" class="col-name">' +
escapeHtml(labels.product) +
"</th>" +
'<th scope="col" class="col-qty text-center">' +
escapeHtml(labels.quantity) +
"</th>" +
'<th scope="col" class="col-price text-right">' +
escapeHtml(labels.price) +
"</th>" +
'<th scope="col" class="col-subtotal text-right">' +
escapeHtml(labels.subtotal) +
"</th>" +
'</tr></thead><tbody id="checkout-summary-tbody"></tbody></table>' +
'<div class="checkout-total-section"><div class="total-row">' +
'<span class="total-label">' + escapeHtml(labels.total) + '</span>' +
'<span class="total-label">' +
escapeHtml(labels.total) +
"</span>" +
'<span class="total-amount" id="checkout-total-amount">€0.00</span>' +
'</div></div>';
"</div></div>";
summaryDiv.innerHTML = html;
summaryTable = summaryDiv.querySelector('.checkout-summary-table');
tbody = summaryDiv.querySelector('#checkout-summary-tbody');
totalSection = summaryDiv.querySelector('.checkout-total-section');
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 = '';
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 = '<td colspan="4" class="text-center text-muted py-4">' +
var emptyRow = document.createElement("tr");
emptyRow.id = "checkout-empty-row";
emptyRow.className = "empty-message";
emptyRow.innerHTML =
'<td colspan="4" class="text-center text-muted py-4">' +
'<i class="fa fa-inbox fa-2x mb-2"></i>' +
'<p>' + escapeHtml(labels.empty) + '</p>' +
'</td>';
"<p>" +
escapeHtml(labels.empty) +
"</p>" +
"</td>";
tbody.appendChild(emptyRow);
// Hide total section
totalSection.style.display = 'none';
totalSection.style.display = "none";
} else {
// Hide empty row if visible
var emptyRow = tbody.querySelector('#checkout-empty-row');
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;
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) {
Object.keys(cart).forEach(function (productId) {
if (productId === deliveryProductId) {
deliveryProduct = { id: productId, item: cart[productId] };
} else {
@ -217,14 +256,14 @@
});
// Sort normal products numerically
normalProducts.sort(function(a, b) {
normalProducts.sort(function (a, b) {
return parseInt(a.id) - parseInt(b.id);
});
var total = 0;
// Render normal products first
normalProducts.forEach(function(product) {
normalProducts.forEach(function (product) {
var item = product.item;
var qty = parseFloat(item.quantity || item.qty || 1);
if (isNaN(qty)) qty = 1;
@ -233,11 +272,20 @@
var subtotal = qty * price;
total += subtotal;
var row = document.createElement('tr');
row.innerHTML = '<td>' + escapeHtml(item.name) + '</td>' +
'<td class="text-center">' + qty.toFixed(2).replace(/\.?0+$/, '') + '</td>' +
'<td class="text-right">€' + price.toFixed(2) + '</td>' +
'<td class="text-right">€' + subtotal.toFixed(2) + '</td>';
var row = document.createElement("tr");
row.innerHTML =
"<td>" +
escapeHtml(item.name) +
"</td>" +
'<td class="text-center">' +
qty.toFixed(2).replace(/\.?0+$/, "") +
"</td>" +
'<td class="text-right">€' +
price.toFixed(2) +
"</td>" +
'<td class="text-right">€' +
subtotal.toFixed(2) +
"</td>";
tbody.appendChild(row);
});
@ -251,32 +299,41 @@
var subtotal = qty * price;
total += subtotal;
var row = document.createElement('tr');
row.innerHTML = '<td>' + escapeHtml(item.name) + '</td>' +
'<td class="text-center">' + qty.toFixed(2).replace(/\.?0+$/, '') + '</td>' +
'<td class="text-right">€' + price.toFixed(2) + '</td>' +
'<td class="text-right">€' + subtotal.toFixed(2) + '</td>';
var row = document.createElement("tr");
row.innerHTML =
"<td>" +
escapeHtml(item.name) +
"</td>" +
'<td class="text-center">' +
qty.toFixed(2).replace(/\.?0+$/, "") +
"</td>" +
'<td class="text-right">€' +
price.toFixed(2) +
"</td>" +
'<td class="text-right">€' +
subtotal.toFixed(2) +
"</td>";
tbody.appendChild(row);
}
// Update total
var totalAmount = summaryDiv.querySelector('#checkout-total-amount');
var totalAmount = summaryDiv.querySelector("#checkout-total-amount");
if (totalAmount) {
totalAmount.textContent = '€' + total.toFixed(2);
totalAmount.textContent = "€" + total.toFixed(2);
}
// Show total section
totalSection.style.display = 'block';
totalSection.style.display = "block";
}
console.log('[CHECKOUT] Summary rendered');
console.log("[CHECKOUT] Summary rendered");
};
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
var div = document.createElement('div');
var div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}

View file

@ -3,9 +3,7 @@
* This file is kept for backwards compatibility but is no longer needed.
* The main renderSummary() logic is in checkout_labels.js
*/
(function() {
'use strict';
(function () {
"use strict";
// Checkout rendering is handled by checkout_labels.js
})();

View file

@ -3,56 +3,65 @@
* Manages home delivery checkbox and product addition/removal
*/
(function() {
'use strict';
(function () {
"use strict";
var HomeDeliveryManager = {
deliveryProductId: null,
deliveryProductPrice: 5.74,
deliveryProductName: 'Home Delivery', // Default fallback
deliveryProductName: "Home Delivery", // Default fallback
orderId: null,
homeDeliveryEnabled: false,
init: function() {
init: function () {
// Get delivery product info from data attributes
var checkoutPage = document.querySelector('.eskaera-checkout-page');
var checkoutPage = document.querySelector(".eskaera-checkout-page");
if (checkoutPage) {
this.deliveryProductId = checkoutPage.getAttribute('data-delivery-product-id');
console.log('[HomeDelivery] deliveryProductId from attribute:', this.deliveryProductId, 'type:', typeof this.deliveryProductId);
var price = checkoutPage.getAttribute('data-delivery-product-price');
this.deliveryProductId = checkoutPage.getAttribute("data-delivery-product-id");
console.log(
"[HomeDelivery] deliveryProductId from attribute:",
this.deliveryProductId,
"type:",
typeof this.deliveryProductId
);
var price = checkoutPage.getAttribute("data-delivery-product-price");
if (price) {
this.deliveryProductPrice = parseFloat(price);
}
// Get translated product name from data attribute (auto-translated by Odoo server)
var productName = checkoutPage.getAttribute('data-delivery-product-name');
var productName = checkoutPage.getAttribute("data-delivery-product-name");
if (productName) {
this.deliveryProductName = productName;
console.log('[HomeDelivery] Using translated product name from server:', this.deliveryProductName);
console.log(
"[HomeDelivery] Using translated product name from server:",
this.deliveryProductName
);
}
// Check if home delivery is enabled for this order
var homeDeliveryAttr = checkoutPage.getAttribute('data-home-delivery-enabled');
this.homeDeliveryEnabled = homeDeliveryAttr === 'true' || homeDeliveryAttr === 'True';
console.log('[HomeDelivery] Home delivery enabled:', this.homeDeliveryEnabled);
var homeDeliveryAttr = checkoutPage.getAttribute("data-home-delivery-enabled");
this.homeDeliveryEnabled =
homeDeliveryAttr === "true" || homeDeliveryAttr === "True";
console.log("[HomeDelivery] Home delivery enabled:", this.homeDeliveryEnabled);
// Show/hide home delivery section based on configuration
this.toggleHomeDeliverySection();
}
// Get order ID from confirm button
var confirmBtn = document.getElementById('confirm-order-btn');
var confirmBtn = document.getElementById("confirm-order-btn");
if (confirmBtn) {
this.orderId = confirmBtn.getAttribute('data-order-id');
console.log('[HomeDelivery] orderId from button:', this.orderId);
this.orderId = confirmBtn.getAttribute("data-order-id");
console.log("[HomeDelivery] orderId from button:", this.orderId);
}
var checkbox = document.getElementById('home-delivery-checkbox');
var checkbox = document.getElementById("home-delivery-checkbox");
if (!checkbox) return;
var self = this;
checkbox.addEventListener('change', function() {
checkbox.addEventListener("change", function () {
if (this.checked) {
self.addDeliveryProduct();
self.showDeliveryInfo();
@ -66,42 +75,44 @@
this.checkDeliveryInCart();
},
toggleHomeDeliverySection: function() {
var homeDeliverySection = document.querySelector('[id*="home-delivery"], [class*="home-delivery"]');
var checkbox = document.getElementById('home-delivery-checkbox');
var homeDeliveryContainer = document.getElementById('home-delivery-container');
toggleHomeDeliverySection: function () {
var homeDeliverySection = document.querySelector(
'[id*="home-delivery"], [class*="home-delivery"]'
);
var checkbox = document.getElementById("home-delivery-checkbox");
var homeDeliveryContainer = document.getElementById("home-delivery-container");
if (this.homeDeliveryEnabled) {
// Show home delivery option
if (checkbox) {
checkbox.closest('.form-check').style.display = 'block';
checkbox.closest(".form-check").style.display = "block";
}
if (homeDeliveryContainer) {
homeDeliveryContainer.style.display = 'block';
homeDeliveryContainer.style.display = "block";
}
console.log('[HomeDelivery] Home delivery option shown');
console.log("[HomeDelivery] Home delivery option shown");
} else {
// Hide home delivery option and delivery info alert
if (checkbox) {
checkbox.closest('.form-check').style.display = 'none';
checkbox.closest(".form-check").style.display = "none";
checkbox.checked = false;
}
if (homeDeliveryContainer) {
homeDeliveryContainer.style.display = 'none';
homeDeliveryContainer.style.display = "none";
}
// Also hide the delivery info alert when home delivery is disabled
this.hideDeliveryInfo();
this.removeDeliveryProduct();
console.log('[HomeDelivery] Home delivery option and delivery info hidden');
console.log("[HomeDelivery] Home delivery option and delivery info hidden");
}
},
checkDeliveryInCart: function() {
checkDeliveryInCart: function () {
if (!this.deliveryProductId) return;
var cart = this.getCart();
if (cart[this.deliveryProductId]) {
var checkbox = document.getElementById('home-delivery-checkbox');
var checkbox = document.getElementById("home-delivery-checkbox");
if (checkbox) {
checkbox.checked = true;
this.showDeliveryInfo();
@ -109,93 +120,103 @@
}
},
getCart: function() {
getCart: function () {
if (!this.orderId) return {};
var cartKey = 'eskaera_' + this.orderId + '_cart';
var cartKey = "eskaera_" + this.orderId + "_cart";
var cartStr = localStorage.getItem(cartKey);
return cartStr ? JSON.parse(cartStr) : {};
},
saveCart: function(cart) {
saveCart: function (cart) {
if (!this.orderId) return;
var cartKey = 'eskaera_' + this.orderId + '_cart';
var cartKey = "eskaera_" + this.orderId + "_cart";
localStorage.setItem(cartKey, JSON.stringify(cart));
// Re-render checkout summary without reloading
var self = this;
setTimeout(function() {
setTimeout(function () {
// Use the global function from checkout_labels.js
if (typeof window.renderCheckoutSummary === 'function') {
if (typeof window.renderCheckoutSummary === "function") {
window.renderCheckoutSummary();
}
}, 50);
},
renderCheckoutSummary: function() {
renderCheckoutSummary: function () {
// Stub - now handled by global window.renderCheckoutSummary
},
addDeliveryProduct: function() {
addDeliveryProduct: function () {
if (!this.deliveryProductId) {
console.warn('[HomeDelivery] Delivery product ID not found');
console.warn("[HomeDelivery] Delivery product ID not found");
return;
}
console.log('[HomeDelivery] Adding delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
console.log(
"[HomeDelivery] Adding delivery product - deliveryProductId:",
this.deliveryProductId,
"orderId:",
this.orderId
);
var cart = this.getCart();
console.log('[HomeDelivery] Current cart before adding:', cart);
console.log("[HomeDelivery] Current cart before adding:", cart);
cart[this.deliveryProductId] = {
id: this.deliveryProductId,
name: this.deliveryProductName,
price: this.deliveryProductPrice,
qty: 1
qty: 1,
};
console.log('[HomeDelivery] Cart after adding delivery:', cart);
console.log("[HomeDelivery] Cart after adding delivery:", cart);
this.saveCart(cart);
console.log('[HomeDelivery] Delivery product added to localStorage');
console.log("[HomeDelivery] Delivery product added to localStorage");
},
removeDeliveryProduct: function() {
removeDeliveryProduct: function () {
if (!this.deliveryProductId) {
console.warn('[HomeDelivery] Delivery product ID not found');
console.warn("[HomeDelivery] Delivery product ID not found");
return;
}
console.log('[HomeDelivery] Removing delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
console.log(
"[HomeDelivery] Removing delivery product - deliveryProductId:",
this.deliveryProductId,
"orderId:",
this.orderId
);
var cart = this.getCart();
console.log('[HomeDelivery] Current cart before removing:', cart);
console.log("[HomeDelivery] Current cart before removing:", cart);
if (cart[this.deliveryProductId]) {
delete cart[this.deliveryProductId];
console.log('[HomeDelivery] Cart after removing delivery:', cart);
console.log("[HomeDelivery] Cart after removing delivery:", cart);
}
this.saveCart(cart);
console.log('[HomeDelivery] Delivery product removed from localStorage');
console.log("[HomeDelivery] Delivery product removed from localStorage");
},
showDeliveryInfo: function() {
var alert = document.getElementById('delivery-info-alert');
showDeliveryInfo: function () {
var alert = document.getElementById("delivery-info-alert");
if (alert) {
console.log('[HomeDelivery] Showing delivery info alert');
alert.classList.remove('d-none');
alert.style.display = 'block';
console.log("[HomeDelivery] Showing delivery info alert");
alert.classList.remove("d-none");
alert.style.display = "block";
}
},
hideDeliveryInfo: function() {
var alert = document.getElementById('delivery-info-alert');
hideDeliveryInfo: function () {
var alert = document.getElementById("delivery-info-alert");
if (alert) {
console.log('[HomeDelivery] Hiding delivery info alert');
alert.classList.add('d-none');
alert.style.display = 'none';
console.log("[HomeDelivery] Hiding delivery info alert");
alert.classList.add("d-none");
alert.style.display = "none";
}
}
},
};
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
HomeDeliveryManager.init();
});
} else {

View file

@ -1,30 +1,30 @@
/**
* DEPRECATED: Use i18n_manager.js instead
*
*
* This file is kept for backwards compatibility only.
* All translation logic has been moved to i18n_manager.js which
* fetches translations from the server endpoint /eskaera/i18n
*
*
* Migration guide:
* OLD: window.getCheckoutLabels()
* NEW: i18nManager.getAll()
*
*
* OLD: window.formatCurrency(amount)
* NEW: i18nManager.formatCurrency(amount)
*
*
* Copyright 2025 Criptomart
* License AGPL-3.0 or later
*/
(function() {
'use strict';
(function () {
"use strict";
// Keep legacy functions as wrappers for backwards compatibility
/**
* DEPRECATED - Use i18nManager.getAll() or i18nManager.get(key) instead
*/
window.getCheckoutLabels = function(key) {
window.getCheckoutLabels = function (key) {
if (window.i18nManager && window.i18nManager.initialized) {
if (key) {
return window.i18nManager.get(key);
@ -38,30 +38,29 @@
/**
* DEPRECATED - Use i18nManager.getAll() instead
*/
window.getSearchLabels = function() {
window.getSearchLabels = function () {
if (window.i18nManager && window.i18nManager.initialized) {
return {
'searchPlaceholder': window.i18nManager.get('search_products'),
'noResults': window.i18nManager.get('no_results')
searchPlaceholder: window.i18nManager.get("search_products"),
noResults: window.i18nManager.get("no_results"),
};
}
return {
'searchPlaceholder': 'Search products...',
'noResults': 'No products found'
searchPlaceholder: "Search products...",
noResults: "No products found",
};
};
/**
* DEPRECATED - Use i18nManager.formatCurrency(amount) instead
*/
window.formatCurrency = function(amount) {
window.formatCurrency = function (amount) {
if (window.i18nManager) {
return window.i18nManager.formatCurrency(amount);
}
// Fallback
return '€' + parseFloat(amount).toFixed(2);
return "€" + parseFloat(amount).toFixed(2);
};
console.log('[i18n_helpers] DEPRECATED - Use i18n_manager.js instead');
console.log("[i18n_helpers] DEPRECATED - Use i18n_manager.js instead");
})();

View file

@ -1,21 +1,21 @@
/**
* I18N Manager - Unified Translation Management
*
*
* Single point of truth for all translations.
* Fetches from server endpoint /eskaera/i18n once and caches.
*
*
* Usage:
* i18nManager.init().then(function() {
* var translated = i18nManager.get('product'); // Returns translated string
* var allLabels = i18nManager.getAll(); // Returns all labels
* });
*
*
* Copyright 2025 Criptomart
* License AGPL-3.0 or later
*/
(function() {
'use strict';
(function () {
"use strict";
window.i18nManager = {
labels: null,
@ -26,7 +26,7 @@
* Initialize by fetching translations from server
* Returns a Promise that resolves when translations are loaded
*/
init: function() {
init: function () {
if (this.initialized) {
return Promise.resolve();
}
@ -38,41 +38,45 @@
var self = this;
// Detect user's language from document or fallback to en_US
var detectedLang = document.documentElement.lang || 'es_ES';
console.log('[i18nManager] Detected language:', detectedLang);
var detectedLang = document.documentElement.lang || "es_ES";
console.log("[i18nManager] Detected language:", detectedLang);
// Fetch translations from server
this.initPromise = fetch('/eskaera/i18n', {
method: 'POST',
this.initPromise = fetch("/eskaera/i18n", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({ lang: detectedLang })
body: JSON.stringify({ lang: detectedLang }),
})
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP error, status = ' + response.status);
}
return response.json();
})
.then(function(data) {
// Handle JSON-RPC response format
// Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } }
// Extract the actual labels from the result property
var labels = data.result || data;
console.log('[i18nManager] ✓ Loaded', Object.keys(labels).length, 'translation labels');
self.labels = labels;
self.initialized = true;
return labels;
})
.catch(function(error) {
console.error('[i18nManager] Error loading translations:', error);
// Fallback to empty object so app doesn't crash
self.labels = {};
self.initialized = true;
return {};
});
.then(function (response) {
if (!response.ok) {
throw new Error("HTTP error, status = " + response.status);
}
return response.json();
})
.then(function (data) {
// Handle JSON-RPC response format
// Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } }
// Extract the actual labels from the result property
var labels = data.result || data;
console.log(
"[i18nManager] ✓ Loaded",
Object.keys(labels).length,
"translation labels"
);
self.labels = labels;
self.initialized = true;
return labels;
})
.catch(function (error) {
console.error("[i18nManager] Error loading translations:", error);
// Fallback to empty object so app doesn't crash
self.labels = {};
self.initialized = true;
return {};
});
return this.initPromise;
},
@ -81,9 +85,9 @@
* Get a specific translation label
* Returns the translated string or the key if not found
*/
get: function(key) {
get: function (key) {
if (!this.initialized) {
console.warn('[i18nManager] Not yet initialized. Call init() first.');
console.warn("[i18nManager] Not yet initialized. Call init() first.");
return key;
}
return this.labels[key] || key;
@ -92,9 +96,9 @@
/**
* Get all translation labels as object
*/
getAll: function() {
getAll: function () {
if (!this.initialized) {
console.warn('[i18nManager] Not yet initialized. Call init() first.');
console.warn("[i18nManager] Not yet initialized. Call init() first.");
return {};
}
return this.labels;
@ -103,7 +107,7 @@
/**
* Check if a specific label exists
*/
has: function(key) {
has: function (key) {
if (!this.initialized) return false;
return key in this.labels;
},
@ -111,43 +115,42 @@
/**
* Format currency to Euro format
*/
formatCurrency: function(amount) {
formatCurrency: function (amount) {
try {
return new Intl.NumberFormat(document.documentElement.lang || 'es_ES', {
style: 'currency',
currency: 'EUR'
return new Intl.NumberFormat(document.documentElement.lang || "es_ES", {
style: "currency",
currency: "EUR",
}).format(amount);
} catch (e) {
// Fallback to simple Euro format
return '€' + parseFloat(amount).toFixed(2);
return "€" + parseFloat(amount).toFixed(2);
}
},
/**
* Escape HTML to prevent XSS
*/
escapeHtml: function(text) {
if (!text) return '';
var div = document.createElement('div');
escapeHtml: function (text) {
if (!text) return "";
var div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
},
};
// Auto-initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
i18nManager.init().catch(function(err) {
console.error('[i18nManager] Auto-init failed:', err);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
i18nManager.init().catch(function (err) {
console.error("[i18nManager] Auto-init failed:", err);
});
});
} else {
// DOM already loaded
setTimeout(function() {
i18nManager.init().catch(function(err) {
console.error('[i18nManager] Auto-init failed:', err);
setTimeout(function () {
i18nManager.init().catch(function (err) {
console.error("[i18nManager] Auto-init failed:", err);
});
}, 100);
}
})();