Quantity control layout: changed from wrapping flex to deliberate two-row
grid so the add-to-cart button sits full-width underneath the stepper,
avoiding accidental line breaks and giving the primary action more pulsing
area. Also removed the /Kg suffix's redundant font rules.
Order card delivery row: reordered to badge-then-date (visually centred),
removed the "Delivery" label, and swapped bg-primary to text-bg-primary for
proper contrast on the home-delivery badge (3.13:1 minimum).
Tooltip translations: moved hardcoded static tooltips from data-bs-title
(untranslatable) to title (QWeb-translatable). Handled the edge case of
"Save Cart" and "Back to Cart" which had translations but no model_terms
reference in the POT, causing the merge to discard them — added the view
reference so translations now apply.
Load-from-history page: added accessibility: a visible status message
("Loading your order…"), a <noscript> fallback with link to the group
order, lang and viewport meta tags, and localised strings for all three
languages. Gave the page a minimal inline style block since it does not
inherit the token system (no website.layout).
Translations: 11 new entries (es/eu/ca) for new/reworded UI strings, plus
two code catalogue entries (products found, Close) that needed the
#. odoo-python comment for _() resolution. Documented the silent-failure
pattern in docs/TRANSLATIONS.md: the POT merge, untranslatable attributes,
and missing code comments.
Tests: 246 passing. Pre-commit: clean.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- group.order.home_delivery is a stored compute field with no inverse;
Odoo still lets write() set it directly, so a stray direct write stuck
instead of always being re-derived from delivery_product_id. create()
and write() now strip it from vals, same pattern already used for slug.
- _find_recent_draft_order only bounded drafts by create_date, so a
freshly created draft for a stale/previous pickup_date (but created
"now") was wrongly reused. Fix requires both create_date to fall in
the active window AND pickup_date to match when set — the latter
alone isn't enough either, per the regression already covered by
test_find_recent_draft_excludes_previous_cycle (observed in
production at stage.elikabilbo.eus).
Add ca.po for both addons, matching the existing es/eu coverage
(378 and 4 entries respectively). Add the missing "ca" fallback block
in website_sale_i18n.py alongside the existing es/eu ones, and correct
the i18n README, which falsely claimed complete pt/gl/fr/it coverage
that was never actually present.
- create(): slugs were only checked against the database, so records created
in the same batch (duplicating several orders from the list view, importing
rows sharing a name) collided on group_order_slug_uniq. _generate_unique_slug
now also skips the slugs already handed out in the batch.
- _redirect_to_slug_url(): `post` is passed as a dict instead of splatted, so a
query parameter named `suffix` no longer binds to the keyword argument of the
same name (HTTP 500 on /eskaera/<id>/checkout?suffix=x, and a corrupted path
on the shop route).
- post-migrate: the backfill runs with tracking_disable, `slug` is tracked and
the upgrade posted a chatter message on every pre-existing order.
Tests for the two reachable cases: batch create and query parameters kept
across the legacy redirect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Group order pages were published under their database id (`/eskaera/1`), which
says nothing to the member opening the link. `group.order` gains a `slug` field
and the public pages move to `/eskaera/<slug>` (e.g. `/eskaera/escola-fructuos`).
The slug is generated from the order name on creation, is unique, and can be
edited under the name on the form; emptying it regenerates it from the current
name. Values that would make the order unreachable are rejected: a plain number
(the legacy numeric URLs win that match) and the static routes served under
`/eskaera/` (`labels`, `save-order`, `i18n`, ...).
`/eskaera/<id>` and `/eskaera/<id>/checkout` are kept as redirects to their slug
URL, so the links already shared with members keep working. The AJAX endpoints
(`load-page`, `save-order`, `confirm`, ...) stay numeric: they never show up in
the address bar. Consequently the frontend now reads the order id from the
`data-order-id` attribute only, as the URL no longer carries it.
The post-migration script fills the slug of the orders that already existed.
Renaming the `/eskaera` prefix itself (`/escolas` for a schools deployment)
needs no code: a *308 Redirect / Rewrite* rule per public route in Website >
Configuration > Redirects serves the pages on the new prefix, rewrites the
links in the templates and redirects the old URLs, per website. Documented in
`readme/CONFIGURE.rst`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "shop as a read-only catalog" behaviour lived inside website_sale_aplicoop,
so every site that wanted the eskaera flow also lost the standard cart. It now
ships as its own installable addon, with the redirect target configurable
instead of hardcoded to /eskaera.
- website_sale_disable_cart: hides the cart UI (12 header styles + the product
card quick-add) and overrides the standard cart endpoints. Redirect URL is
configurable in Website settings (default /shop); only site-internal paths are
accepted, so a misconfigured value cannot turn the shop into an open redirect
nor loop back into a disabled route.
- Fixes carried over from the original code: /shop/cart/quantity is the Odoo 18
path (it was /shop/cart_quantity, which never matched), the boxed, sidebar and
sales two/three/four headers were not covered (the cart link stayed visible on
them), and the routes now override the standard methods instead of registering
duplicate ones.
- website_sale_aplicoop 18.0.1.12.0: drops the view file and the four redirect
routes; installing it no longer touches the standard shop.
Upgrade order matters: update website_sale_aplicoop first, then install
website_sale_disable_cart in a second Odoo run — both use the same XPaths and
obsolete records are only cleaned up at the end of a run.
Tests: 8/8 in website_sale_disable_cart, aplicoop unaffected (its 2 failures
predate this change). Verified live on a DB clone: /shop/cart returns 303 to the
configured URL and no cart markup remains on /shop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El precio por unidad de las tarjetas usaba el campo nativo base_unit_price
(list_price sin impuestos), mostrando un importe distinto al precio
principal de la tarjeta, que sí incluye IVA. Ahora se calcula a partir del
precio ya impositado. Además se extiende el sufijo de precio por unidad
("/L") a productos vendidos por litro, que ya tenían el step de 0.1 pero
no el indicador visual.
One-time, biweekly and monthly group orders now follow the same cron
confirmation flow as weekly ones (confirm sale orders + batch pickings
when the cycle cutoff passes):
- Biweekly/monthly keep the cutoff_day/pickup_day weekday scheme on a
recurrence grid anchored at start_date (creation date as fallback):
cutoffs advance +14 days / +1 month snapped to cutoff_day, with
catch-up after cron downtime. Previously they behaved as weekly.
- One-time orders (specials/promotions) are driven by end_date
(cutoff_date = end_date); once passed, the cron confirms, batches
and closes the group order.
- end_date keeps its "empty = permanent" meaning for recurring orders.
- Website draft-cart lookup window is now period-aware instead of
assuming a 6-day weekly cycle.
- New cron tests for once/biweekly/monthly cycles; i18n es/eu updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Group orders are not confirmed until the cutoff date, so draft/sent
sale.order lines never generate stock.moves and are invisible to
virtual_available. This change makes the shop aware of that demand.
- group.order._compute_draft_sale_demand: queries sale.order.line in
draft/sent state (mirroring sale_stock forecasted report logic) and
returns pending demand per product.id in the product's own UoM.
- _get_products_for_group_order: delegates to new _apply_stock_filter_and_sort
which excludes storable products whose forecasted net qty
(virtual_available − draft demand) <= 0, unless allow_out_of_stock_order.
- _compute_stock_ribbons: reads draft_demand_by_product from ORM context
so is_out_of_stock / is_low_stock / dynamic_ribbon_id reflect net qty.
- Controller: new _prepare_draft_stock_data helper calculates demand once
per request, injects context, and builds product_max_qty dict. Applied
in eskaera_shop, load_eskaera_page and load_products_ajax.
- Template: qty input gets max and data-max-qty from product_max_qty.
- JS: blocks add-to-cart if requested quantity exceeds data-max-qty.
- Fixes type check: type=='consu' → is_storable=True (Odoo 18 semantics).
- 21 new tests in test_forecasted_stock.py covering demand calculation,
ribbon logic with context, and group order filtering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
965c5c2 stored the cart as {cutoff_date, items: {...}} in
eskaera_<id>_cart. That broke every other reader of the same key:
- checkout_labels.js iterates Object.keys() expecting product IDs and
rendered "cutoff_date" / "items" as ghost rows → users on the
checkout page saw their cart as empty.
- home_delivery.js read/wrote the cart in place; the in-place mutation
destroyed the wrapper.
- _saveOrderDraft serialised the same object straight to the server,
POSTing "cutoff_date" and "items" as productIds.
Split the schema: eskaera_<id>_cart keeps the plain {productId: {...}}
shape every other JS file already relies on; the cycle marker moves to
eskaera_<id>_cart_cycle. _loadCart migrates browsers still holding the
v18.0.1.10.0 wrapped value on the next read.
Also make eviction strictly opt-in: drop the cart only when we know
the current cycle AND the stored cycle disagrees. Missing data on
either side (check-status didn't run, XHR failed) → preserve. This
restores the checkout page where check-status isn't called.
Version bump triggers the in-tab auto-reload added in 6e6d1e5 for
clients already on that build.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When we ship a JS-only fix, users with a long-lived tab keep running
the old code until they hard-refresh — there is no clean way to push
new code to an already-loaded page. Now /eskaera/check-status returns
the module's installed_version as client_version, and the eskaera
page embeds the same version in data-build-version on the cart
container. The JS captures the page's build version at init; on every
check-status response it compares them and triggers window.location.
reload() on mismatch. A sessionStorage timestamp guards against
reload loops if the versions stay disagreeing (cached HTML upstream).
The version bump in __manifest__.py also invalidates the asset bundle
URL hash, so even users without this signalling path get fresh JS on
their next navigation.
This is forward-looking: clients on the old JS (no check) still need
one manual refresh today, but every future deploy will auto-recover.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The cart was reappearing on stage with ghost items that had never been
saved as drafts on the server. Root cause: the localStorage cart had no
cycle awareness. Users who added items but never clicked Save left the
items in localStorage forever, and the only existing eviction path
(_clearCurrentOrderCartSilently via _checkGroupOrderStatus) only fires
when cutoff_passed flips true — which it rarely does, because the
stored cutoff_date is normally in the future for an active cycle. On
the next visit, _loadCart happily rehydrated the stale items and
_autoLoadDraftOnInit short-circuited because the cart was no longer
empty.
Stamp every cart written to localStorage with the cutoff_date it
belongs to and reject on cycle mismatch in _loadCart. The cutoff_date
is captured from /eskaera/check-status (which already returned it).
Legacy unstamped values and corrupt JSON are also dropped, so existing
browsers self-heal on first reload after the deploy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eskaera shop cards now display the standard website_sale ribbon
(variant_ribbon_id or website_ribbon_id, e.g. "Novedad") in addition
to the dynamic stock ribbon. Stock ribbons move to the right so they
coexist with user-defined ribbons (which default to the left).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The /eskaera/load-draft endpoint was returning previous-cycle drafts when
group_order.cutoff_date was still in the future but group_order.pickup_date
had not yet been recomputed (its compute only depends on pickup_day and
start_date). Both the stale group_order.pickup_date and the old draft's
pickup_date held the same past value, so the exact-pickup-date filter in
_find_recent_draft_order matched the stale draft and the cart was
repopulated with old products immediately after being cleared.
Replace the pickup_date exact-match + current-week fallback with a single
cutoff-anchored window: create_date in [cutoff_date - 6 days, cutoff_date].
Drafts created outside that window belong to a previous cycle and must
not be reused. The change applies to all four callers (load-draft,
clear-cart, save-order, confirm) so the merge/confirm paths also stop
attaching to stale drafts.
Covered by a new regression test that mirrors the production setup
(matching pickup_date, draft create_date backdated 10 days).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously, when a user reopened a group order whose cutoff day had
already passed, the /eskaera/check-status response correctly triggered
the localStorage cart clear, but _autoLoadDraftOnInit immediately
re-fetched the previous cycle's draft sale.order from /eskaera/load-draft
(which only guarded on group_order.state, not cutoff_date) and the stale
items reappeared in the cart, confusing users.
Add a cutoff_date < today guard to load_draft_cart so the endpoint
returns the existing clear_cart unavailable response, and short-circuit
_autoLoadDraftOnInit on the frontend via a _skipDraftAutoLoad flag set
in _checkGroupOrderStatus to avoid the now-pointless XHR round trip.
Covered by a new regression test in tests/test_group_order_status_endpoint.py.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace old pickup_day/pickup_date display with the slot-based logic
(pickup_slot_ids) on the checkout page, consistent with the shop page.
Falls back to legacy pickup_day if no slots are configured.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add hardcoded fallback translations for es/eu when PO-based translation
returns the source string unchanged. Expand labels dict with all keys
needed by the frontend. Fix JSON response parsing in template
(data.result || data). Add js_translations keys. Add pot file for
stock_picking_batch_custom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The checkout home-delivery checkbox had a fixed checked="checked"
attribute, so it always rendered checked even when the delivery
product was not in the cart. Remove it and let checkDeliveryInCart()
mark it only when the delivery product is actually present.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add sudo() to pricelist_item and fiscal position fallback in _get_pricing_info
so portal users can price the delivery product without triggering an AccessError
on account.tax. Remove the redundant #home-delivery-btn click handler from
website_sale.js — home_delivery.js already owns that button via
bindShopHomeDeliveryButton(), which manages the active class and localStorage cart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs prevented home_delivery from reaching sale.order:
1. #home-delivery-btn (shop sidebar) had no JS handler — clicking it did
nothing. Now it toggles active state and persists choice to sessionStorage.
2. _executeSaveCartAsDraft (Save Cart button) never included is_delivery in
the request body. Now reads the toggle button state (or the page-level
data-home-delivery-enabled fallback) and sends is_delivery correctly.
3. #home-delivery-checkbox on checkout page was unchecked by default and
always shown. Now it is pre-checked when group_order.home_delivery is
True, wrapped in t-if to hide it when delivery is not configured, and
synced bidirectionally with sessionStorage so the shop-page toggle state
carries over to checkout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New test_home_delivery.py: verifies group.order.home_delivery derives
from delivery_product_id (compute), and that sale.order.home_delivery
is set correctly through _get_effective_delivery_context
- Fix test_save_order_endpoints: replace home_delivery=True (now ignored
on computed field) with delivery_product_id to enable delivery
- Fix test_phase3_confirm_eskaera: same fix for integration test setUp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Make group.order.home_delivery a computed stored field that is True
whenever delivery_product_id is set, eliminating the manual checkbox
that could be forgotten. This fixes sale.order.home_delivery not being
set when the delivery product was configured but the boolean was left
unchecked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Antes: si el slot tenía label, solo se mostraba el label (sin día ni hora).
Ahora: siempre se muestra día + rango horario (09:00–13:00), con el label
entre paréntesis como información adicional opcional.
Aplicado en eskaera_order_card_meta (tarjeta compacta) y en la vista
detalle de la tienda (group_order.pickup_slot_ids).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El operador | en expresiones t-if de QWeb es el OR bitwise de Python,
no un filtro Jinja2. Como 'length' no existe en el contexto QWeb, evaluaba
recordset | None y lanzaba TypeError. Simplificado a solo pickup_slot_ids
(recordset vacío ya es falsy en Python).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Añadir reglas de acceso para group.order.slot en ir.model.access.csv
- Añadir pestaña "Pickup Slots" en el formulario de group.order con lista
editable (handle de secuencia, día, etiqueta, hora inicio/fin, activo)
- Corregir valores del campo weekday: de dígitos numéricos a nombres de día
(Monday...Sunday) para mejor usabilidad
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- product-img-cover: max-height → height fija para que placeholder y imagen
real ocupen exactamente el mismo bloque (120px/90px/60px según breakpoint)
- product-img-placeholder: reemplaza SVG inline por flex centrado, más limpio
- Reducir padding/márgenes generales en card-body, title, supplier, tags y precio
- Dos breakpoints responsivos: ≤768px (tablet, imagen 90px) y ≤480px (móvil,
imagen 60px, fuentes y márgenes mínimos)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- README.md: reescrito con tabla completa de los 14 addons (6 OCA + 8 custom),
versiones actuales, árbol de dependencias y comandos de desarrollo
- docs/README.md: simplificado a índice limpio, eliminadas referencias rotas
- website_sale_aplicoop/CHANGELOG.md: añadidas versiones 1.7.0, 1.8.0 y 1.9.0
con los cambios agrupados por temática desde el último registro (1.6.0)
- website_sale_aplicoop/README_DEV.md: reescrito para reflejar v1.9.0 —
modelos actuales (group.order.slot), controladores /eskaera, catálogo
whitelist/blacklist, lazy loading, crons y árbol de dependencias
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add sequence field (default 10) to product.category with _order = "sequence, name"
- Inherit product.category tree view to add drag-handle widget
- Sort category hierarchy and available categories by sequence in controller
- Migration 18.0.1.9.0: add sequence column to product_category table
- Bump version to 18.0.1.9.0
- Backend: Agregar método _validate_items_for_group_order() para validar que los productos históricos sigan siendo disponibles en la orden de grupo actual
- Backend: Modificar load_order_from_history() para filtrar solo items disponibles antes de pasar al template
- Backend: Generar mensaje de aviso traducido cuando hay productos no disponibles
- Template: Pasar información de productos no disponibles y warnings al JavaScript
- Frontend: Mostrar notificación de advertencia si hubo productos excluidos durante la carga histórica
- Notas: Esto evita cargar productos que ya no existen en la orden actual debido a cambios en categorías, proveedores o listas negras
Evita conflicto de tipo de ruta con el método clear_cart() del padre
WebsiteSale de Odoo 18 (type=json). Misma URL /eskaera/clear-cart,
solo cambia el nombre del método Python.
También añade noqa C901 en save_eskaera_draft (complejidad preexistente).
- Convertir 4 tests de decorador @patch a context manager 'with patch(...)' para evitar RuntimeError en LocalProxy de Werkzeug
- Corregir patrón env(user=..., context=dict(...)) en Odoo 18 (sin .with_context())
- Agregar website real al mock para integración con helpers de pricing (_get_pricing_info)
- Añadir pickup_date en fixture de existing_order para que _find_recent_draft_order localice correctamente
- BUGFIX: Agregar (5,) a order_line para limpiar líneas previas al actualizar pedido existente
Resultado: 0 failed, 0 errors de 4 tests en Docker para TestConfirmEskaera_Integration
BREAKING: _create_or_update_sale_order ahora limpia las líneas anteriores con (5,) antes de asignar las nuevas cuando se actualiza un pedido existente. Comportamiento previo (duplicación de líneas) era un bug.
Añade botón 'Clear Cart' (fa-trash) en el header y footer del sidebar
del carrito en la página de lista de productos.
Cambios:
- views/website_templates.xml: botón clear-cart-btn en card-header y
clear-cart-btn-footer en card-footer del sidebar
- controllers/website_sale.py: nuevo endpoint POST /eskaera/clear-cart
que cancela el sale.order borrador del usuario si existe
- static/src/js/website_sale.js: método _clearCart(), listeners para
ambos botones (header + footer)
- models/js_translations.py: nuevas cadenas clear_cart, clear_cart_confirm,
cart_cleared, draft_cancelled
- i18n/es.po, i18n/eu.po: traducciones ES y EU de los nuevos labels