2777 lines
112 KiB
Python
2777 lines
112 KiB
Python
# Copyright 2025 Criptomart
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from datetime import timedelta
|
|
|
|
from odoo import _
|
|
from odoo import http
|
|
from odoo.http import request
|
|
|
|
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AplicoopWebsiteSale(WebsiteSale):
|
|
"""Controlador personalizado para website_sale de Aplicoop.
|
|
|
|
Sustitución de la antigua aplicación Aplicoop:
|
|
https://sourceforge.net/projects/aplicoop/
|
|
"""
|
|
|
|
def _get_day_names(self, env=None):
|
|
"""Get translated day names list (0=Monday to 6=Sunday).
|
|
|
|
Gets day names from fields_get() which returns the selection values
|
|
TRANSLATED according to the user's current language preference.
|
|
|
|
Returns: list of 7 translated day names in the user's language
|
|
"""
|
|
if env is None:
|
|
from odoo.http import request
|
|
|
|
env = request.env
|
|
|
|
# Log context language for debugging
|
|
context_lang = env.context.get("lang", "NO_LANG")
|
|
_logger.info("📅 _get_day_names called with context lang: %s", context_lang)
|
|
|
|
group_order_model = env["group.order"]
|
|
# Use fields_get() to get field definitions WITH translations applied
|
|
fields = group_order_model.fields_get(["pickup_day"])
|
|
selection_options = fields.get("pickup_day", {}).get("selection", [])
|
|
|
|
# Log the actual day names returned
|
|
day_names = [name for value, name in selection_options]
|
|
_logger.info(
|
|
"📅 Returning day names: %s",
|
|
day_names[:3] if len(day_names) >= 3 else day_names,
|
|
)
|
|
|
|
return day_names
|
|
|
|
def _get_next_date_for_weekday(self, weekday_num, start_date=None):
|
|
"""Calculate the next occurrence of a given weekday (0=Monday to 6=Sunday).
|
|
|
|
Args:
|
|
weekday_num: int, 0=Monday, 6=Sunday
|
|
start_date: datetime.date, starting point (defaults to today)
|
|
|
|
Returns:
|
|
datetime.date of the next occurrence of that weekday
|
|
"""
|
|
if start_date is None:
|
|
start_date = datetime.now().date()
|
|
|
|
# Convert int weekday (0=Mon) to Python's weekday (0=Mon is same)
|
|
target_weekday = int(weekday_num)
|
|
current_weekday = start_date.weekday()
|
|
|
|
# Calculate days until target weekday
|
|
days_ahead = target_weekday - current_weekday
|
|
if days_ahead <= 0: # Target day has already occurred this week
|
|
days_ahead += 7
|
|
|
|
return start_date + timedelta(days=days_ahead)
|
|
|
|
def _get_detected_language(self, **post):
|
|
"""Detect user language from multiple sources with fallback priority.
|
|
|
|
Priority:
|
|
1. URL parameter 'lang'
|
|
2. POST JSON parameter 'lang'
|
|
3. HTTP Cookie 'lang'
|
|
4. request.env.context['lang']
|
|
5. User's language preference
|
|
6. Default: 'es_ES'
|
|
|
|
Returns: str - language code (e.g., 'es_ES', 'eu_ES', 'en_US')
|
|
"""
|
|
url_lang = request.params.get("lang")
|
|
post_lang = post.get("lang")
|
|
cookie_lang = request.httprequest.cookies.get("lang")
|
|
context_lang = request.env.context.get("lang")
|
|
user_lang = request.env.user.lang or "es_ES"
|
|
|
|
detected = None
|
|
if url_lang:
|
|
detected = url_lang
|
|
elif post_lang:
|
|
detected = post_lang
|
|
elif cookie_lang:
|
|
detected = cookie_lang
|
|
elif context_lang:
|
|
detected = context_lang
|
|
else:
|
|
detected = user_lang
|
|
|
|
_logger.info(
|
|
"🌐 Language detection: url=%s, post=%s, cookie=%s, context=%s, user=%s → DETECTED=%s",
|
|
url_lang,
|
|
post_lang,
|
|
cookie_lang,
|
|
context_lang,
|
|
user_lang,
|
|
detected,
|
|
)
|
|
return detected
|
|
|
|
def _get_translated_labels(self, lang=None):
|
|
"""Get ALL translated UI labels and messages unified.
|
|
|
|
This is the SINGLE SOURCE OF TRUTH for all user-facing messages.
|
|
Every endpoint that returns JSON should use this to get consistent translations.
|
|
|
|
Args:
|
|
lang: str - language code (defaults to detected language)
|
|
|
|
Returns: dict - ALL translated labels and messages
|
|
"""
|
|
if lang is None:
|
|
lang = self._get_detected_language()
|
|
|
|
# Create a new environment with the target language context
|
|
# This is the correct way in Odoo to get translations in a specific language
|
|
env_lang = request.env(context=dict(request.env.context, lang=lang))
|
|
|
|
# Use the imported _ function which respects the environment context
|
|
# The strings must exist in models/js_translations.py
|
|
labels = {
|
|
# ============ SUMMARY TABLE LABELS ============
|
|
"product": env_lang._("Product"),
|
|
"quantity": env_lang._("Quantity"),
|
|
"price": env_lang._("Price"),
|
|
"subtotal": env_lang._("Subtotal"),
|
|
"total": env_lang._("Total"),
|
|
"empty": env_lang._("This order's cart is empty."),
|
|
"empty_cart": env_lang._("Your cart is empty"),
|
|
# ============ ACTION LABELS ============
|
|
"add_to_cart": env_lang._("Add to Cart"),
|
|
"remove_from_cart": env_lang._("Remove from Cart"),
|
|
"remove_item": env_lang._("Remove Item"),
|
|
"save_cart": env_lang._("Save Cart"),
|
|
"reload_cart": env_lang._("Reload Cart"),
|
|
"load_draft": env_lang._("Load Draft"),
|
|
"proceed_to_checkout": env_lang._("Proceed to Checkout"),
|
|
"confirm_order": env_lang._("Confirm Order"),
|
|
"back_to_cart": env_lang._("Back to Cart"),
|
|
# ============ MODAL CONFIRMATION LABELS ============
|
|
"confirmation": env_lang._("Confirmation"),
|
|
"cancel": env_lang._("Cancel"),
|
|
"confirm": env_lang._("Confirm"),
|
|
"merge": env_lang._("Merge"),
|
|
"replace": env_lang._("Replace"),
|
|
"draft_merge_btn": env_lang._("Merge"),
|
|
"draft_replace_btn": env_lang._("Replace"),
|
|
# ============ SUCCESS MESSAGES ============
|
|
"draft_saved_success": env_lang._("Cart saved as draft successfully"),
|
|
"draft_loaded_success": env_lang._("Draft order loaded successfully"),
|
|
"draft_merged_success": env_lang._("Draft merged successfully"),
|
|
"draft_replaced_success": env_lang._("Draft replaced successfully"),
|
|
"order_confirmed": env_lang._("Thank you! Your order has been confirmed."),
|
|
"order_loaded": env_lang._("Order loaded"),
|
|
"cart_restored": env_lang._("Your cart has been restored"),
|
|
"qty_updated": env_lang._("Quantity updated"),
|
|
# ============ ERROR MESSAGES ============
|
|
"error_save_draft": env_lang._("Error saving cart"),
|
|
"error_load_draft": env_lang._("Error loading draft"),
|
|
"error_confirm_order": env_lang._("Error confirming order"),
|
|
"error_processing_response": env_lang._("Error processing response"),
|
|
"error_connection": env_lang._("Connection error"),
|
|
"error_unknown": env_lang._("Unknown error"),
|
|
"error_invalid_data": env_lang._("Invalid data provided"),
|
|
"error_order_not_found": env_lang._("Order not found"),
|
|
"error_no_draft_orders": env_lang._("No draft orders found for this week"),
|
|
"invalid_quantity": env_lang._("Please enter a valid quantity"),
|
|
# ============ CONFIRMATION MESSAGES ============
|
|
"save_draft_confirm": env_lang._(
|
|
"Are you sure you want to save this cart as draft?\n\nItems to save: "
|
|
),
|
|
"save_draft_reload": env_lang._(
|
|
"You will be able to reload this cart later."
|
|
),
|
|
"reload_draft_confirm": env_lang._(
|
|
"Are you sure you want to load your last saved draft?"
|
|
),
|
|
"reload_draft_replace": env_lang._(
|
|
"This will replace the current items in your cart"
|
|
),
|
|
"reload_draft_with": env_lang._("with the saved draft."),
|
|
# ============ DRAFT MODAL LABELS ============
|
|
"draft_already_exists": env_lang._("Draft Already Exists"),
|
|
"draft_exists_message": env_lang._(
|
|
"A saved draft already exists for this week."
|
|
),
|
|
"draft_two_options": env_lang._("You have two options:"),
|
|
"draft_option1_title": env_lang._("Option 1: Merge with Existing Draft"),
|
|
"draft_option1_desc": env_lang._(
|
|
"Combine your current cart with the existing draft."
|
|
),
|
|
"draft_existing_items": env_lang._("Existing draft has"),
|
|
"draft_current_items": env_lang._("Current cart has"),
|
|
"draft_items_count": env_lang._("item(s)"),
|
|
"draft_merge_note": env_lang._(
|
|
"Products will be merged by adding quantities. If a product exists in both, quantities will be combined."
|
|
),
|
|
"draft_option2_title": env_lang._("Option 2: Replace with Current Cart"),
|
|
"draft_option2_desc": env_lang._(
|
|
"Delete the old draft and save only the current cart items."
|
|
),
|
|
"draft_replace_warning": env_lang._(
|
|
"The existing draft will be permanently deleted."
|
|
),
|
|
# ============ CHECKOUT PAGE LABELS ============
|
|
"home_delivery": env_lang._("Home Delivery"),
|
|
"delivery_information": env_lang._("Delivery Information"),
|
|
"delivery_info_template": env_lang._(
|
|
"Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}"
|
|
),
|
|
"important": env_lang._("Important"),
|
|
"confirm_order_warning": env_lang._(
|
|
"Once you confirm this order, you will not be able to modify it. Please review carefully before confirming."
|
|
),
|
|
# ============ PORTAL PAGE LABELS ============
|
|
"load_in_cart": env_lang._("Load in Cart"),
|
|
"consumer_group": env_lang._("Consumer Group"),
|
|
"delivery_date": env_lang._("Delivery Date:"),
|
|
"pickup_date": env_lang._("Pickup Date:"),
|
|
"delivery_notice": env_lang._("Delivery Notice:"),
|
|
"no_delivery_instructions": env_lang._("No special delivery instructions"),
|
|
"pickup_location": env_lang._("Pickup Location:"),
|
|
# ============ DAY NAMES (FOR PORTAL) ============
|
|
"monday": env_lang._("Monday"),
|
|
"tuesday": env_lang._("Tuesday"),
|
|
"wednesday": env_lang._("Wednesday"),
|
|
"thursday": env_lang._("Thursday"),
|
|
"friday": env_lang._("Friday"),
|
|
"saturday": env_lang._("Saturday"),
|
|
"sunday": env_lang._("Sunday"),
|
|
# ============ CATEGORY FILTER ============
|
|
"browse_categories": env_lang._("Browse Product Categories"),
|
|
"all_categories": env_lang._("All categories"),
|
|
"categories": env_lang._("Categories"),
|
|
# ============ SEARCH LABELS ============
|
|
"search": env_lang._("Search"),
|
|
"search_products": env_lang._("Search products..."),
|
|
"no_results": env_lang._("No products found"),
|
|
# ============ MISC ============
|
|
"items": env_lang._("items"),
|
|
"added_to_cart": env_lang._("added to cart"),
|
|
}
|
|
|
|
return labels
|
|
|
|
def _build_category_hierarchy(self, categories):
|
|
"""Organiza las categorías en una estructura jerárquica padre-hijo.
|
|
|
|
Args:
|
|
categories: product.category recordset
|
|
|
|
Returns:
|
|
list de dicts con estructura: {
|
|
'id': category_id,
|
|
'name': category_name,
|
|
'parent_id': parent_id,
|
|
'children': [list of child dicts]
|
|
}
|
|
"""
|
|
if not categories:
|
|
return []
|
|
|
|
# Crear mapa de categorías por ID
|
|
category_map = {}
|
|
for cat in categories:
|
|
category_map[cat.id] = {
|
|
"id": cat.id,
|
|
"name": cat.name,
|
|
"parent_id": cat.parent_id.id if cat.parent_id else None,
|
|
"children": [],
|
|
}
|
|
|
|
# Identificar categorías raíz (sin padre en la lista) y organizar jerarquía
|
|
roots = []
|
|
for _cat_id, cat_info in category_map.items():
|
|
parent_id = cat_info["parent_id"]
|
|
|
|
# Si el padre no está en la lista de categorías disponibles, es una raíz
|
|
if parent_id is None or parent_id not in category_map:
|
|
roots.append(cat_info)
|
|
else:
|
|
# Agregar a los hijos de su padre
|
|
category_map[parent_id]["children"].append(cat_info)
|
|
|
|
# Ordenar raíces y sus hijos por nombre
|
|
def sort_hierarchy(items):
|
|
items.sort(key=lambda x: x["name"])
|
|
for item in items:
|
|
if item["children"]:
|
|
sort_hierarchy(item["children"])
|
|
|
|
sort_hierarchy(roots)
|
|
return roots
|
|
|
|
# ========== PHASE 1: HELPER METHODS FOR VALIDATION AND CONFIGURATION ==========
|
|
|
|
def _resolve_pricelist(self):
|
|
"""Resolve the pricelist to use for pricing.
|
|
|
|
Resolution order:
|
|
1. Aplicoop configured pricelist (from settings)
|
|
2. Website current pricelist
|
|
3. First active pricelist (fallback)
|
|
|
|
Returns:
|
|
product.pricelist record or False if none found
|
|
"""
|
|
pricelist = None
|
|
|
|
# Try to get configured Aplicoop pricelist first
|
|
try:
|
|
aplicoop_pricelist_id = (
|
|
request.env["ir.config_parameter"]
|
|
.sudo()
|
|
.get_param("website_sale_aplicoop.pricelist_id")
|
|
)
|
|
if aplicoop_pricelist_id:
|
|
pricelist = request.env["product.pricelist"].browse(
|
|
int(aplicoop_pricelist_id)
|
|
)
|
|
if pricelist.exists():
|
|
_logger.info(
|
|
"_resolve_pricelist: Using configured Aplicoop pricelist: %s (id=%s)",
|
|
pricelist.name,
|
|
pricelist.id,
|
|
)
|
|
return pricelist
|
|
else:
|
|
_logger.warning(
|
|
"_resolve_pricelist: Configured Aplicoop pricelist (id=%s) not found",
|
|
aplicoop_pricelist_id,
|
|
)
|
|
except Exception as err:
|
|
_logger.warning(
|
|
"_resolve_pricelist: Error getting Aplicoop pricelist: %s", str(err)
|
|
)
|
|
|
|
# Fallback to website pricelist
|
|
try:
|
|
pricelist = request.website._get_current_pricelist()
|
|
if pricelist:
|
|
_logger.info(
|
|
"_resolve_pricelist: Using website pricelist: %s (id=%s)",
|
|
pricelist.name,
|
|
pricelist.id,
|
|
)
|
|
return pricelist
|
|
except Exception as err:
|
|
_logger.warning(
|
|
"_resolve_pricelist: Error getting website pricelist: %s", str(err)
|
|
)
|
|
|
|
# Final fallback to first active pricelist
|
|
pricelist = request.env["product.pricelist"].search(
|
|
[("active", "=", True)], limit=1
|
|
)
|
|
if pricelist:
|
|
_logger.info(
|
|
"_resolve_pricelist: Using first active pricelist: %s (id=%s)",
|
|
pricelist.name,
|
|
pricelist.id,
|
|
)
|
|
return pricelist
|
|
|
|
_logger.error(
|
|
"_resolve_pricelist: ERROR - No pricelist found! Pricing may fail."
|
|
)
|
|
return False
|
|
|
|
def _prepare_product_display_info(self, product, product_price_info):
|
|
"""Prepare all display information for a product in a QWeb-safe way.
|
|
|
|
This function pre-processes all values that might be None or require
|
|
conditional logic, so the template can use simple variable references
|
|
without complex expressions that confuse QWeb's parser.
|
|
|
|
Args:
|
|
product: product.template record
|
|
product_price_info: dict with 'price', 'list_price', etc.
|
|
|
|
Returns:
|
|
dict with all pre-processed display values ready for template
|
|
"""
|
|
# Safety: Get price, ensure it's a float
|
|
price_data = product_price_info.get(product.id, {})
|
|
price = (
|
|
price_data.get("price", product.list_price)
|
|
if price_data
|
|
else product.list_price
|
|
)
|
|
price_safe = float(price) if price else 0.0
|
|
|
|
# Safety: Get UoM category name
|
|
uom_category_name = ""
|
|
if product.uom_id:
|
|
if product.uom_id.category_id:
|
|
uom_category_name = product.uom_id.category_id.name or ""
|
|
|
|
return {
|
|
"display_price": price_safe,
|
|
"safe_uom_category": uom_category_name,
|
|
}
|
|
|
|
def _validate_confirm_request(self, data):
|
|
"""Validate all requirements for confirm order request.
|
|
|
|
Validates:
|
|
- order_id exists and is valid integer
|
|
- group.order exists and is in open state
|
|
- user has associated partner_id
|
|
- items list is not empty
|
|
|
|
Args:
|
|
data: dict with 'order_id' and 'items' keys
|
|
|
|
Returns:
|
|
tuple: (order_id, group_order, current_user)
|
|
|
|
Raises:
|
|
ValueError: if any validation fails
|
|
"""
|
|
# Validate order_id
|
|
order_id = data.get("order_id")
|
|
if not order_id:
|
|
raise ValueError("order_id is required") from None
|
|
|
|
try:
|
|
order_id = int(order_id)
|
|
except (ValueError, TypeError) as err:
|
|
raise ValueError(f"Invalid order_id format: {order_id}") from err
|
|
|
|
# Verify that the group.order exists
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
if not group_order.exists():
|
|
raise ValueError(f"Order {order_id} not found") from None
|
|
|
|
# Verify that the order is in open state
|
|
if group_order.state != "open":
|
|
raise ValueError("Order is not available (not in open state)") from None
|
|
|
|
# Validate user has partner_id
|
|
current_user = request.env.user
|
|
if not current_user.partner_id:
|
|
raise ValueError("User has no associated partner") from None
|
|
|
|
# Validate items
|
|
items = data.get("items", [])
|
|
if not items:
|
|
raise ValueError("No items in cart") from None
|
|
|
|
_logger.info(
|
|
"_validate_confirm_request: Valid request for order %d with %d items",
|
|
order_id,
|
|
len(items),
|
|
)
|
|
|
|
return order_id, group_order, current_user, items
|
|
|
|
def _validate_draft_request(self, data):
|
|
"""Validate all requirements for draft order request.
|
|
|
|
Validates:
|
|
- order_id exists and is valid integer
|
|
- group.order exists
|
|
- user has associated partner_id
|
|
- items list is not empty
|
|
|
|
Args:
|
|
data: dict with 'order_id' and 'items' keys
|
|
|
|
Returns:
|
|
tuple: (order_id, group_order, current_user, items, merge_action, existing_draft_id)
|
|
|
|
Raises:
|
|
ValueError: if any validation fails
|
|
"""
|
|
# Validate order_id
|
|
order_id = data.get("order_id")
|
|
if not order_id:
|
|
raise ValueError("order_id is required")
|
|
|
|
try:
|
|
order_id = int(order_id)
|
|
except (ValueError, TypeError) as err:
|
|
raise ValueError(f"Invalid order_id format: {order_id}") from err
|
|
|
|
# Verify that the group.order exists
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
if not group_order.exists():
|
|
raise ValueError(f"Order {order_id} not found")
|
|
|
|
# Validate user has partner_id
|
|
current_user = request.env.user
|
|
if not current_user.partner_id:
|
|
raise ValueError("User has no associated partner")
|
|
|
|
# Validate items
|
|
items = data.get("items", [])
|
|
if not items:
|
|
raise ValueError("No items in cart")
|
|
|
|
# Get optional merge/replace parameters
|
|
merge_action = data.get("merge_action")
|
|
existing_draft_id = data.get("existing_draft_id")
|
|
|
|
_logger.info(
|
|
"_validate_draft_request: Valid request for order %d with %d items (merge_action=%s)",
|
|
order_id,
|
|
len(items),
|
|
merge_action,
|
|
)
|
|
|
|
return (
|
|
order_id,
|
|
group_order,
|
|
current_user,
|
|
items,
|
|
merge_action,
|
|
existing_draft_id,
|
|
)
|
|
|
|
def _validate_confirm_json(self, data):
|
|
"""Validate JSON data and order for confirm_eskaera endpoint.
|
|
|
|
Validates:
|
|
- order_id is present and valid integer
|
|
- group.order exists and is in 'open' state
|
|
- user has associated partner_id
|
|
- items list is not empty
|
|
|
|
Args:
|
|
data: dict with 'order_id' and 'items' keys
|
|
|
|
Returns:
|
|
tuple: (order_id, group_order, current_user, items, is_delivery)
|
|
|
|
Raises:
|
|
ValueError: if any validation fails
|
|
"""
|
|
# Validate order_id
|
|
order_id = data.get("order_id")
|
|
if not order_id:
|
|
raise ValueError("order_id is required")
|
|
|
|
try:
|
|
order_id = int(order_id)
|
|
except (ValueError, TypeError) as err:
|
|
raise ValueError(f"Invalid order_id format: {order_id}") from err
|
|
|
|
# Verify that the order exists
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
if not group_order.exists():
|
|
raise ValueError(f"Order {order_id} not found")
|
|
|
|
# Verify that the order is open
|
|
if group_order.state != "open":
|
|
raise ValueError(f"Order is {group_order.state}")
|
|
|
|
# Validate user has partner_id
|
|
current_user = request.env.user
|
|
if not current_user.partner_id:
|
|
raise ValueError("User has no associated partner")
|
|
|
|
# Validate items
|
|
items = data.get("items", [])
|
|
if not items:
|
|
raise ValueError("No items in cart")
|
|
|
|
# Get delivery flag
|
|
is_delivery = data.get("is_delivery", False)
|
|
|
|
_logger.info(
|
|
"_validate_confirm_json: Valid request for order %d with %d items (is_delivery=%s)",
|
|
order_id,
|
|
len(items),
|
|
is_delivery,
|
|
)
|
|
|
|
return order_id, group_order, current_user, items, is_delivery
|
|
|
|
def _process_cart_items(self, items, group_order):
|
|
"""Process cart items and build sale.order line data.
|
|
|
|
Args:
|
|
items: list of item dicts with product_id, quantity, product_price
|
|
group_order: group.order record for context
|
|
|
|
Returns:
|
|
list of (0, 0, line_dict) tuples ready for sale.order creation
|
|
|
|
Raises:
|
|
ValueError: if no valid items after processing
|
|
"""
|
|
sale_order_lines = []
|
|
|
|
for item in items:
|
|
try:
|
|
product_id = int(item.get("product_id"))
|
|
quantity = float(item.get("quantity", 1))
|
|
price = float(item.get("product_price", 0))
|
|
|
|
product = request.env["product.product"].browse(product_id)
|
|
if not product.exists():
|
|
_logger.warning(
|
|
"_process_cart_items: Product %d does not exist", product_id
|
|
)
|
|
continue
|
|
|
|
# Get product name in user's language context
|
|
product_in_lang = product.with_context(lang=request.env.lang)
|
|
product_name = product_in_lang.name
|
|
|
|
line_data = {
|
|
"product_id": product_id,
|
|
"product_uom_qty": quantity,
|
|
"price_unit": price or product.list_price,
|
|
"name": product_name, # Force the translated product name
|
|
}
|
|
_logger.info("_process_cart_items: Adding line: %s", line_data)
|
|
sale_order_lines.append((0, 0, line_data))
|
|
except (ValueError, TypeError) as e:
|
|
_logger.warning(
|
|
"_process_cart_items: Error processing item %s: %s",
|
|
item,
|
|
str(e),
|
|
)
|
|
continue
|
|
|
|
if not sale_order_lines:
|
|
raise ValueError("No valid items in cart")
|
|
|
|
_logger.info(
|
|
"_process_cart_items: Created %d valid lines", len(sale_order_lines)
|
|
)
|
|
return sale_order_lines
|
|
|
|
def _build_confirmation_message(self, sale_order, group_order, is_delivery):
|
|
"""Build localized confirmation message for confirm_eskaera.
|
|
|
|
Translates message and pickup/delivery info according to user's language.
|
|
Handles day names and date formatting.
|
|
|
|
Args:
|
|
sale_order: sale.order record just created
|
|
group_order: group.order record
|
|
is_delivery: boolean indicating if home delivery
|
|
|
|
Returns:
|
|
dict with message, pickup_day, pickup_date, pickup_day_index
|
|
"""
|
|
# Get pickup day index
|
|
try:
|
|
pickup_day_index = int(group_order.pickup_day)
|
|
except Exception:
|
|
pickup_day_index = None
|
|
|
|
# Initialize translatable strings
|
|
base_message = _("Thank you! Your order has been confirmed.")
|
|
order_reference_label = _("Order reference")
|
|
pickup_label = _("Pickup day")
|
|
delivery_label = _("Delivery date")
|
|
pickup_day_name = ""
|
|
pickup_date_str = ""
|
|
|
|
# Add order reference to message
|
|
if sale_order.name:
|
|
base_message = (
|
|
f"{base_message}\n\n{order_reference_label}: {sale_order.name}"
|
|
)
|
|
|
|
# Get translated day names
|
|
if pickup_day_index is not None:
|
|
try:
|
|
day_names = self._get_day_names(env=request.env)
|
|
pickup_day_name = day_names[pickup_day_index % len(day_names)]
|
|
except Exception:
|
|
pickup_day_name = ""
|
|
|
|
# Add pickup/delivery date in numeric format
|
|
if group_order.pickup_date:
|
|
if is_delivery:
|
|
# For delivery, use delivery_date (already computed as pickup_date + 1)
|
|
if group_order.delivery_date:
|
|
pickup_date_str = group_order.delivery_date.strftime("%d/%m/%Y")
|
|
# For delivery, use the next day's name
|
|
if pickup_day_index is not None:
|
|
try:
|
|
day_names = self._get_day_names(env=request.env)
|
|
# Get the next day's name for delivery
|
|
next_day_index = (pickup_day_index + 1) % 7
|
|
pickup_day_name = day_names[next_day_index]
|
|
except Exception:
|
|
pickup_day_name = ""
|
|
else:
|
|
pickup_date_str = group_order.pickup_date.strftime("%d/%m/%Y")
|
|
else:
|
|
# For pickup, use the same date
|
|
pickup_date_str = group_order.pickup_date.strftime("%d/%m/%Y")
|
|
|
|
# Build final message with correct label and date based on delivery or pickup
|
|
message = base_message
|
|
label_to_use = delivery_label if is_delivery else pickup_label
|
|
if pickup_day_name and pickup_date_str:
|
|
message = (
|
|
f"{message}\n\n\n{label_to_use}: {pickup_day_name} ({pickup_date_str})"
|
|
)
|
|
elif pickup_day_name:
|
|
message = f"{message}\n\n\n{label_to_use}: {pickup_day_name}"
|
|
elif pickup_date_str:
|
|
message = f"{message}\n\n\n{label_to_use}: {pickup_date_str}"
|
|
|
|
# Log for translation debugging
|
|
try:
|
|
_logger.info(
|
|
"_build_confirmation_message: lang=%s, message=%s",
|
|
request.env.lang,
|
|
message,
|
|
)
|
|
except Exception:
|
|
_logger.info("_build_confirmation_message: message logging failed")
|
|
|
|
return {
|
|
"message": message,
|
|
"pickup_day": pickup_day_name,
|
|
"pickup_date": pickup_date_str,
|
|
"pickup_day_index": pickup_day_index,
|
|
}
|
|
|
|
@http.route(["/eskaera"], type="http", auth="user", website=True)
|
|
def eskaera_list(self, **post):
|
|
"""Página de pedidos de grupo abiertos esta semana.
|
|
|
|
Muestra todos los pedidos abiertos de la compañía del usuario.
|
|
Seguridad controlada por record rule (company_id filtering).
|
|
"""
|
|
group_order_obj = request.env["group.order"]
|
|
current_user = request.env.user
|
|
|
|
# Validate that the user has a partner_id
|
|
if not current_user.partner_id:
|
|
_logger.error("eskaera_list: User %d has no partner_id", current_user.id)
|
|
return request.redirect("/web")
|
|
|
|
# Obtener pedidos activos para esta semana (ya filtrados por company_id via record rule)
|
|
active_orders = group_order_obj.get_active_orders_for_week()
|
|
|
|
_logger.info("=== ESKAERA LIST ===")
|
|
_logger.info("User: %s (ID: %d)", current_user.name, current_user.id)
|
|
_logger.info("User company: %s", current_user.company_id.name)
|
|
_logger.info(
|
|
"Active orders from get_active_orders_for_week: %s",
|
|
active_orders.mapped("name"),
|
|
)
|
|
|
|
return request.render(
|
|
"website_sale_aplicoop.eskaera_page",
|
|
{
|
|
"active_orders": active_orders,
|
|
"day_names": self._get_day_names(env=request.env),
|
|
},
|
|
)
|
|
|
|
def _filter_published_tags(self, tags):
|
|
"""Filter tags to only include those visible on ecommerce."""
|
|
return tags.filtered(lambda t: getattr(t, "visible_on_ecommerce", True))
|
|
|
|
@http.route(["/eskaera/<int:order_id>"], type="http", auth="user", website=True)
|
|
def eskaera_shop(self, order_id, **post):
|
|
"""Página de tienda para un pedido específico (eskaera).
|
|
|
|
Muestra productos del pedido y gestiona el carrito separado.
|
|
Soporta búsqueda y filtrado por categoría.
|
|
"""
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
|
|
if not group_order.exists():
|
|
return request.redirect("/eskaera")
|
|
|
|
# Verificar que el pedido está activo
|
|
if group_order.state != "open":
|
|
return request.redirect("/eskaera")
|
|
|
|
# Seguridad: record rule controla acceso por company_id
|
|
# No additional group validation needed
|
|
|
|
# Print order cutoff date information
|
|
_logger.info("=== ESKAERA SHOP ===")
|
|
_logger.info("Order: %s (ID: %d)", group_order.name, group_order.id)
|
|
_logger.info("Cutoff Day: %s (0=Monday, 6=Sunday)", group_order.cutoff_day)
|
|
_logger.info("Pickup Day: %s", group_order.pickup_day)
|
|
if group_order.start_date:
|
|
_logger.info("Start Date: %s", group_order.start_date.strftime("%Y-%m-%d"))
|
|
if group_order.end_date:
|
|
_logger.info("End Date: %s", group_order.end_date.strftime("%Y-%m-%d"))
|
|
|
|
# Get lazy loading configuration
|
|
lazy_loading_enabled = (
|
|
request.env["ir.config_parameter"].get_param(
|
|
"website_sale_aplicoop.lazy_loading_enabled", "True"
|
|
)
|
|
== "True"
|
|
)
|
|
per_page = int(
|
|
request.env["ir.config_parameter"].get_param(
|
|
"website_sale_aplicoop.products_per_page", 20
|
|
)
|
|
)
|
|
|
|
# Get page parameter (default to 1)
|
|
try:
|
|
page = int(post.get("page", 1))
|
|
if page < 1:
|
|
page = 1
|
|
except (ValueError, TypeError):
|
|
page = 1
|
|
|
|
_logger.info(
|
|
"eskaera_shop: lazy_loading=%s, per_page=%d, page=%d",
|
|
lazy_loading_enabled,
|
|
per_page,
|
|
page,
|
|
)
|
|
|
|
# Collect products from all configured associations:
|
|
# - Explicit products attached to the group order
|
|
# - Products in the selected categories
|
|
# - Products provided by the selected suppliers
|
|
# - Delegate discovery to the order model (centralised logic)
|
|
products = group_order._get_products_for_group_order(group_order.id)
|
|
_logger.info(
|
|
"eskaera_shop order_id=%d, total products=%d (discovered)",
|
|
order_id,
|
|
len(products),
|
|
)
|
|
|
|
# Get all available categories BEFORE filtering (so dropdown always shows all)
|
|
# Include not only product categories but also their parent categories
|
|
product_categories = products.mapped("categ_id").filtered(lambda c: c.id > 0)
|
|
|
|
# Collect all categories including parent chain
|
|
all_categories_set = set()
|
|
|
|
def collect_category_and_parents(category):
|
|
"""Recursively collect category and all its parent categories."""
|
|
if category and category.id > 0:
|
|
all_categories_set.add(category.id)
|
|
if category.parent_id:
|
|
collect_category_and_parents(category.parent_id)
|
|
|
|
for cat in product_categories:
|
|
collect_category_and_parents(cat)
|
|
|
|
# Convert IDs back to recordset, filtering out id=0
|
|
available_categories = request.env["product.category"].browse(
|
|
list(all_categories_set)
|
|
)
|
|
available_categories = sorted(set(available_categories), key=lambda c: c.name)
|
|
|
|
# Build hierarchical category structure with parent/child relationships
|
|
category_hierarchy = self._build_category_hierarchy(available_categories)
|
|
|
|
# Get search and filter parameters
|
|
search_query = post.get("search", "").strip()
|
|
category_filter = post.get("category", "0")
|
|
|
|
# Apply search
|
|
if search_query:
|
|
products = products.filtered(
|
|
lambda p: search_query.lower() in p.name.lower()
|
|
or search_query.lower() in (p.description or "").lower()
|
|
)
|
|
_logger.info(
|
|
'eskaera_shop: Filtered by search "%s". Found %d',
|
|
search_query,
|
|
len(products),
|
|
)
|
|
|
|
# Apply category filter
|
|
if category_filter != "0":
|
|
try:
|
|
category_id = int(category_filter)
|
|
# Get the selected category
|
|
selected_category = request.env["product.category"].browse(category_id)
|
|
|
|
if selected_category.exists():
|
|
# Get all descendant categories (children, grandchildren, etc.)
|
|
all_category_ids = [category_id]
|
|
|
|
def get_all_children(category):
|
|
for child in category.child_id:
|
|
all_category_ids.append(child.id)
|
|
get_all_children(child)
|
|
|
|
get_all_children(selected_category)
|
|
|
|
# Search for products in the selected category and all descendants
|
|
# This ensures we get products even if the category is a parent with no direct products
|
|
filtered_products = request.env["product.product"].search(
|
|
[
|
|
("categ_id", "in", all_category_ids),
|
|
("active", "=", True),
|
|
("product_tmpl_id.is_published", "=", True),
|
|
("product_tmpl_id.sale_ok", "=", True),
|
|
]
|
|
)
|
|
|
|
# Filter to only include products from the order's permitted categories
|
|
# Get order's permitted category IDs (including descendants)
|
|
if group_order.category_ids:
|
|
order_cat_ids = []
|
|
|
|
def get_order_descendants(categories):
|
|
for cat in categories:
|
|
order_cat_ids.append(cat.id)
|
|
if cat.child_id:
|
|
get_order_descendants(cat.child_id)
|
|
|
|
get_order_descendants(group_order.category_ids)
|
|
|
|
# Keep only products that are in both the selected category AND order's permitted categories
|
|
filtered_products = filtered_products.filtered(
|
|
lambda p: p.categ_id.id in order_cat_ids
|
|
)
|
|
|
|
products = filtered_products
|
|
_logger.info(
|
|
"eskaera_shop: Filtered by category %d and descendants. Found %d products",
|
|
category_id,
|
|
len(products),
|
|
)
|
|
except (ValueError, TypeError) as e:
|
|
_logger.warning("eskaera_shop: Invalid category filter: %s", str(e))
|
|
|
|
# Apply pagination if lazy loading enabled
|
|
total_products = len(products)
|
|
has_next = False
|
|
if lazy_loading_enabled:
|
|
offset = (page - 1) * per_page
|
|
products = products[offset : offset + per_page]
|
|
has_next = offset + per_page < total_products
|
|
_logger.info(
|
|
"eskaera_shop: Paginated - page=%d, offset=%d, per_page=%d, has_next=%s",
|
|
page,
|
|
offset,
|
|
per_page,
|
|
has_next,
|
|
)
|
|
|
|
# Prepare supplier info dict: {product.id: 'Supplier (City)'}
|
|
product_supplier_info = {}
|
|
for product in products:
|
|
supplier_name = ""
|
|
if product.seller_ids:
|
|
partner = product.seller_ids[0].partner_id.sudo()
|
|
supplier_name = partner.name or ""
|
|
if partner.city:
|
|
supplier_name += f" ({partner.city})"
|
|
product_supplier_info[product.id] = supplier_name
|
|
|
|
# Get pricelist and calculate prices with taxes using Odoo's pricelist system
|
|
_logger.info("eskaera_shop: Starting price calculation for order %d", order_id)
|
|
pricelist = self._resolve_pricelist()
|
|
|
|
# Log pricelist selection status
|
|
if pricelist:
|
|
_logger.info(
|
|
"eskaera_shop: Using pricelist %s (id=%s, currency=%s)",
|
|
pricelist.name,
|
|
pricelist.id,
|
|
pricelist.currency_id.name if pricelist.currency_id else "None",
|
|
)
|
|
else:
|
|
_logger.error(
|
|
"eskaera_shop: ERROR - No pricelist found! All prices will use list_price as fallback."
|
|
)
|
|
|
|
product_price_info = {}
|
|
for product in products:
|
|
# Get combination info with taxes calculated using OCA product_get_price_helper
|
|
product_variant = (
|
|
product.product_variant_ids[0] if product.product_variant_ids else False
|
|
)
|
|
if product_variant and pricelist:
|
|
try:
|
|
# Use OCA _get_price method - more robust and complete
|
|
price_info = product_variant._get_price(
|
|
qty=1.0,
|
|
pricelist=pricelist,
|
|
fposition=request.website.fiscal_position_id,
|
|
)
|
|
price = price_info.get("value", 0.0)
|
|
original_price = price_info.get("original_value", 0.0)
|
|
discount = price_info.get("discount", 0.0)
|
|
has_discount = discount > 0
|
|
|
|
product_price_info[product.id] = {
|
|
"price": price,
|
|
"list_price": original_price,
|
|
"has_discounted_price": has_discount,
|
|
"discount": discount,
|
|
"tax_included": price_info.get("tax_included", True),
|
|
}
|
|
_logger.debug(
|
|
"eskaera_shop: Product %s (id=%s) - price=%.2f, original=%.2f, discount=%.1f%%, tax_included=%s",
|
|
product.name,
|
|
product.id,
|
|
price,
|
|
original_price,
|
|
discount,
|
|
price_info.get("tax_included"),
|
|
)
|
|
except Exception as e:
|
|
_logger.warning(
|
|
"eskaera_shop: Error getting price for product %s (id=%s): %s. Using list_price fallback.",
|
|
product.name,
|
|
product.id,
|
|
str(e),
|
|
)
|
|
# Fallback to list_price if _get_price fails
|
|
product_price_info[product.id] = {
|
|
"price": product.list_price,
|
|
"list_price": product.list_price,
|
|
"has_discounted_price": False,
|
|
"discount": 0.0,
|
|
"tax_included": False,
|
|
}
|
|
else:
|
|
# Fallback if no variant or no pricelist
|
|
reason = "no pricelist" if not pricelist else "no variant"
|
|
_logger.info(
|
|
"eskaera_shop: Product %s (id=%s) - Using list_price fallback (reason: %s). Price=%.2f",
|
|
product.name,
|
|
product.id,
|
|
reason,
|
|
product.list_price,
|
|
)
|
|
product_price_info[product.id] = {
|
|
"price": product.list_price,
|
|
"list_price": product.list_price,
|
|
"has_discounted_price": False,
|
|
"discount": 0.0,
|
|
"tax_included": False,
|
|
}
|
|
|
|
# Prepare display info for each product (QWeb-safe: all values pre-processed)
|
|
# This ensures the template can use simple variable references without complex conditionals
|
|
product_display_info = {}
|
|
for product in products:
|
|
display_info = self._prepare_product_display_info(
|
|
product, product_price_info
|
|
)
|
|
product_display_info[product.id] = display_info
|
|
|
|
# Calculate available tags with product count (only show tags that are actually used and visible)
|
|
# Build a dict: {tag_id: {'id': tag_id, 'name': tag_name, 'count': num_products}}
|
|
available_tags_dict = {}
|
|
for product in products:
|
|
for tag in product.product_tag_ids:
|
|
# Only include tags that are visible on ecommerce
|
|
is_visible = getattr(
|
|
tag, "visible_on_ecommerce", True
|
|
) # Default to True if field doesn't exist
|
|
if not is_visible:
|
|
continue
|
|
|
|
if tag.id not in available_tags_dict:
|
|
tag_color = tag.color if tag.color else None
|
|
_logger.info(
|
|
"Tag %s (id=%s): color=%s (type=%s)",
|
|
tag.name,
|
|
tag.id,
|
|
tag_color,
|
|
type(tag_color),
|
|
)
|
|
available_tags_dict[tag.id] = {
|
|
"id": tag.id,
|
|
"name": tag.name,
|
|
"color": tag_color, # Use tag color (hex) or None for theme color
|
|
"count": 0,
|
|
}
|
|
available_tags_dict[tag.id]["count"] += 1
|
|
|
|
# Convert to sorted list of tags (sorted by name for consistent display)
|
|
available_tags = sorted(available_tags_dict.values(), key=lambda t: t["name"])
|
|
_logger.info(
|
|
"eskaera_shop: Found %d available tags for %d products",
|
|
len(available_tags),
|
|
len(products),
|
|
)
|
|
_logger.info("eskaera_shop: available_tags = %s", available_tags)
|
|
|
|
# Manage session for separate cart per order
|
|
session_key = f"eskaera_{order_id}"
|
|
cart = request.session.get(session_key, {})
|
|
|
|
# Get translated labels for JavaScript (same as checkout)
|
|
labels = self.get_checkout_labels()
|
|
|
|
# Filter product tags to only show published ones
|
|
# Create a dictionary with filtered tags for each product
|
|
filtered_products = {}
|
|
for product in products:
|
|
published_tags = self._filter_published_tags(product.product_tag_ids)
|
|
filtered_products[product.id] = {
|
|
"product": product,
|
|
"published_tags": published_tags,
|
|
}
|
|
|
|
return request.render(
|
|
"website_sale_aplicoop.eskaera_shop",
|
|
{
|
|
"group_order": group_order,
|
|
"products": products,
|
|
"filtered_product_tags": filtered_products,
|
|
"cart": cart,
|
|
"available_categories": available_categories,
|
|
"category_hierarchy": category_hierarchy,
|
|
"available_tags": available_tags,
|
|
"search_query": search_query,
|
|
"selected_category": category_filter,
|
|
"day_names": self._get_day_names(env=request.env),
|
|
"product_supplier_info": product_supplier_info,
|
|
"product_price_info": product_price_info,
|
|
"product_display_info": product_display_info,
|
|
"labels": labels,
|
|
"labels_json": json.dumps(labels, ensure_ascii=False),
|
|
"lazy_loading_enabled": lazy_loading_enabled,
|
|
"per_page": per_page,
|
|
"current_page": page,
|
|
"has_next": has_next,
|
|
"total_products": total_products,
|
|
},
|
|
)
|
|
|
|
@http.route(
|
|
["/eskaera/<int:order_id>/load-page"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
methods=["GET"],
|
|
)
|
|
def load_eskaera_page(self, order_id, **post):
|
|
"""Load next page of products for lazy loading.
|
|
|
|
Returns only HTML of product cards without page wrapper.
|
|
"""
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
|
|
if not group_order.exists() or group_order.state != "open":
|
|
return ""
|
|
|
|
# Get lazy loading configuration
|
|
per_page = int(
|
|
request.env["ir.config_parameter"].get_param(
|
|
"website_sale_aplicoop.products_per_page", 20
|
|
)
|
|
)
|
|
|
|
# Get page parameter
|
|
try:
|
|
page = int(post.get("page", 1))
|
|
if page < 1:
|
|
page = 1
|
|
except (ValueError, TypeError):
|
|
page = 1
|
|
|
|
_logger.info(
|
|
"load_eskaera_page: order_id=%d, page=%d, per_page=%d",
|
|
order_id,
|
|
page,
|
|
per_page,
|
|
)
|
|
|
|
# Get all products (same logic as eskaera_shop)
|
|
products = group_order._get_products_for_group_order(group_order.id)
|
|
|
|
# Get pricelist
|
|
pricelist = self._resolve_pricelist()
|
|
|
|
# Calculate prices only for products on this page
|
|
offset = (page - 1) * per_page
|
|
products_page = products[offset : offset + per_page]
|
|
has_next = offset + per_page < len(products)
|
|
|
|
product_price_info = {}
|
|
for product in products_page:
|
|
product_variant = (
|
|
product.product_variant_ids[0] if product.product_variant_ids else False
|
|
)
|
|
if product_variant and pricelist:
|
|
try:
|
|
price_info = product_variant._get_price(
|
|
qty=1.0,
|
|
pricelist=pricelist,
|
|
fposition=request.website.fiscal_position_id,
|
|
)
|
|
price = price_info.get("value", 0.0)
|
|
original_price = price_info.get("original_value", 0.0)
|
|
discount = price_info.get("discount", 0.0)
|
|
has_discount = discount > 0
|
|
|
|
product_price_info[product.id] = {
|
|
"price": price,
|
|
"list_price": original_price,
|
|
"has_discounted_price": has_discount,
|
|
"discount": discount,
|
|
"tax_included": price_info.get("tax_included", True),
|
|
}
|
|
except Exception as e:
|
|
_logger.warning(
|
|
"load_eskaera_page: Error getting price for product %s: %s",
|
|
product.name,
|
|
str(e),
|
|
)
|
|
product_price_info[product.id] = {
|
|
"price": product.list_price,
|
|
"list_price": product.list_price,
|
|
"has_discounted_price": False,
|
|
"discount": 0.0,
|
|
"tax_included": False,
|
|
}
|
|
else:
|
|
product_price_info[product.id] = {
|
|
"price": product.list_price,
|
|
"list_price": product.list_price,
|
|
"has_discounted_price": False,
|
|
"discount": 0.0,
|
|
"tax_included": False,
|
|
}
|
|
|
|
# Prepare supplier info
|
|
product_supplier_info = {}
|
|
for product in products_page:
|
|
supplier_name = ""
|
|
if product.seller_ids:
|
|
partner = product.seller_ids[0].partner_id.sudo()
|
|
supplier_name = partner.name or ""
|
|
if partner.city:
|
|
supplier_name += f" ({partner.city})"
|
|
product_supplier_info[product.id] = supplier_name
|
|
|
|
# Filter product tags
|
|
filtered_products = {}
|
|
for product in products_page:
|
|
published_tags = self._filter_published_tags(product.product_tag_ids)
|
|
filtered_products[product.id] = {
|
|
"product": product,
|
|
"published_tags": published_tags,
|
|
}
|
|
|
|
# Prepare display info for each product (QWeb-safe: all values pre-processed)
|
|
product_display_info = {}
|
|
for product in products_page:
|
|
display_info = self._prepare_product_display_info(
|
|
product, product_price_info
|
|
)
|
|
product_display_info[product.id] = display_info
|
|
|
|
# Get labels
|
|
labels = self.get_checkout_labels()
|
|
|
|
# Render only the products HTML snippet (no page wrapper)
|
|
return request.render(
|
|
"website_sale_aplicoop.eskaera_shop_products",
|
|
{
|
|
"group_order": group_order,
|
|
"products": products_page,
|
|
"filtered_product_tags": filtered_products,
|
|
"product_supplier_info": product_supplier_info,
|
|
"product_price_info": product_price_info,
|
|
"product_display_info": product_display_info,
|
|
"labels": labels,
|
|
"has_next": has_next,
|
|
"next_page": page + 1,
|
|
},
|
|
)
|
|
|
|
@http.route(
|
|
["/eskaera/add-to-cart"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
methods=["POST"],
|
|
csrf=False,
|
|
)
|
|
def add_to_eskaera_cart(self, **post):
|
|
"""Validate and confirm product addition to cart.
|
|
|
|
The cart is managed in localStorage on the frontend.
|
|
This endpoint only validates that the product exists in the order.
|
|
"""
|
|
import json
|
|
|
|
try:
|
|
# Get JSON data from the request body
|
|
data = (
|
|
json.loads(request.httprequest.data) if request.httprequest.data else {}
|
|
)
|
|
|
|
order_id = int(data.get("order_id", 0))
|
|
product_id = int(data.get("product_id", 0))
|
|
quantity = float(data.get("quantity", 1))
|
|
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
product = request.env["product.product"].browse(product_id)
|
|
|
|
# Validate that the order exists and is open
|
|
if not group_order.exists() or group_order.state != "open":
|
|
_logger.warning(
|
|
"add_to_eskaera_cart: Order %d not available (exists=%s, state=%s)",
|
|
order_id,
|
|
group_order.exists(),
|
|
group_order.state if group_order.exists() else "N/A",
|
|
)
|
|
return request.make_response(
|
|
json.dumps({"error": "Order is not available"}),
|
|
[("Content-Type", "application/json")],
|
|
)
|
|
|
|
# Validate that the product is available in this order (use discovery logic)
|
|
available_products = group_order._get_products_for_group_order(
|
|
group_order.id
|
|
)
|
|
if product not in available_products:
|
|
_logger.warning(
|
|
"add_to_eskaera_cart: Product %d not available in order %d",
|
|
product_id,
|
|
order_id,
|
|
)
|
|
return request.make_response(
|
|
json.dumps({"error": "Product not available in this order"}),
|
|
[("Content-Type", "application/json")],
|
|
)
|
|
|
|
# Validate quantity
|
|
if quantity <= 0:
|
|
return request.make_response(
|
|
json.dumps({"error": "Quantity must be greater than 0"}),
|
|
[("Content-Type", "application/json")],
|
|
)
|
|
|
|
_logger.info(
|
|
"add_to_eskaera_cart: Added product %d (qty=%f) to order %d",
|
|
product_id,
|
|
quantity,
|
|
order_id,
|
|
)
|
|
|
|
# Get price with taxes using pricelist
|
|
_logger.info(
|
|
"add_to_eskaera_cart: Getting price for product %s (id=%s)",
|
|
product.name,
|
|
product_id,
|
|
)
|
|
pricelist = None
|
|
|
|
# Resolve pricelist using centralized helper
|
|
pricelist = self._resolve_pricelist()
|
|
|
|
if not pricelist:
|
|
_logger.error(
|
|
"add_to_eskaera_cart: ERROR - No pricelist found! Using list_price for product %s",
|
|
product.name,
|
|
)
|
|
|
|
product_variant = (
|
|
product.product_variant_ids[0] if product.product_variant_ids else False
|
|
)
|
|
|
|
if product_variant and pricelist:
|
|
try:
|
|
# Use OCA _get_price method - more robust and complete
|
|
price_info = product_variant._get_price(
|
|
qty=quantity,
|
|
pricelist=pricelist,
|
|
fposition=request.website.fiscal_position_id,
|
|
)
|
|
price_with_tax = price_info.get("value", product.list_price)
|
|
_logger.info(
|
|
"add_to_eskaera_cart: Product %s - Price: %.2f (original: %.2f, discount: %.1f%%)",
|
|
product.name,
|
|
price_with_tax,
|
|
price_info.get("original_value", 0),
|
|
price_info.get("discount", 0),
|
|
)
|
|
except Exception as e:
|
|
_logger.warning(
|
|
"add_to_eskaera_cart: Error getting price for product %s: %s. Using list_price=%.2f",
|
|
product.name,
|
|
str(e),
|
|
product.list_price,
|
|
)
|
|
else:
|
|
reason = "no pricelist" if not pricelist else "no variant"
|
|
_logger.info(
|
|
"add_to_eskaera_cart: Product %s - Using list_price fallback (reason: %s). Price=%.2f",
|
|
product.name,
|
|
reason,
|
|
price_with_tax,
|
|
)
|
|
|
|
response_data = {
|
|
"success": True,
|
|
"message": f'{_("%s added to cart") % product.name}',
|
|
"product_id": product_id,
|
|
"quantity": quantity,
|
|
"price": price_with_tax,
|
|
}
|
|
return request.make_response(
|
|
json.dumps(response_data), [("Content-Type", "application/json")]
|
|
)
|
|
|
|
except ValueError as e:
|
|
_logger.error("add_to_eskaera_cart: ValueError: %s", str(e))
|
|
return request.make_response(
|
|
json.dumps({"error": f"Invalid parameters: {str(e)}"}),
|
|
[("Content-Type", "application/json")],
|
|
)
|
|
except Exception as e:
|
|
_logger.error("add_to_eskaera_cart: Exception: %s", str(e), exc_info=True)
|
|
return request.make_response(json.dumps({"error": f"Error: {str(e)}"}))
|
|
|
|
@http.route(
|
|
["/eskaera/<int:order_id>/checkout"], type="http", auth="user", website=True
|
|
)
|
|
def eskaera_checkout(self, order_id, **post):
|
|
"""Checkout page to close the cart for the order (eskaera)."""
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
|
|
if not group_order.exists():
|
|
return request.redirect("/eskaera")
|
|
|
|
# Verificar que el pedido está activo
|
|
if group_order.state != "open":
|
|
return request.redirect("/eskaera")
|
|
|
|
# Los datos del carrito vienen desde localStorage en el frontend
|
|
# Esta página solo muestra resumen y botón de confirmación
|
|
|
|
# DEBUG: Log ALL delivery fields
|
|
_logger.warning("=== ESKAERA_CHECKOUT DELIVERY DEBUG ===")
|
|
_logger.warning("group_order.id: %s", group_order.id)
|
|
_logger.warning("group_order.name: %s", group_order.name)
|
|
_logger.warning(
|
|
"group_order.pickup_day: %s (type: %s)",
|
|
group_order.pickup_day,
|
|
type(group_order.pickup_day),
|
|
)
|
|
_logger.warning(
|
|
"group_order.pickup_date: %s (type: %s)",
|
|
group_order.pickup_date,
|
|
type(group_order.pickup_date),
|
|
)
|
|
_logger.warning(
|
|
"group_order.delivery_date: %s (type: %s)",
|
|
group_order.delivery_date,
|
|
type(group_order.delivery_date),
|
|
)
|
|
_logger.warning("group_order.home_delivery: %s", group_order.home_delivery)
|
|
_logger.warning("group_order.delivery_notice: %s", group_order.delivery_notice)
|
|
if group_order.pickup_date:
|
|
_logger.warning(
|
|
"pickup_date formatted: %s",
|
|
group_order.pickup_date.strftime("%d/%m/%Y"),
|
|
)
|
|
_logger.warning("========================================")
|
|
|
|
# Get delivery product ID and name (translated to user's language)
|
|
delivery_product = request.env.ref(
|
|
"website_sale_aplicoop.product_home_delivery", raise_if_not_found=False
|
|
)
|
|
delivery_product_id = delivery_product.id if delivery_product else None
|
|
# Get translated product name based on current language
|
|
if delivery_product:
|
|
delivery_product_translated = delivery_product.with_context(
|
|
lang=request.env.lang
|
|
)
|
|
delivery_product_name = delivery_product_translated.name
|
|
else:
|
|
delivery_product_name = "Home Delivery"
|
|
|
|
# Get all translated labels for JavaScript (same as shop page)
|
|
# This includes all 37 labels: modal labels, confirmation, notifications, cart buttons, etc.
|
|
labels = self.get_checkout_labels()
|
|
|
|
# Convert to JSON string for safe embedding in script tag
|
|
labels_json = json.dumps(labels, ensure_ascii=False)
|
|
|
|
# Prepare template context with explicit debug info
|
|
template_context = {
|
|
"group_order": group_order,
|
|
"day_names": self._get_day_names(env=request.env),
|
|
"delivery_product_id": delivery_product_id,
|
|
"delivery_product_name": delivery_product_name, # Auto-translated to user's language
|
|
"delivery_product_price": (
|
|
delivery_product.list_price if delivery_product else 5.74
|
|
),
|
|
"labels": labels,
|
|
"labels_json": labels_json,
|
|
}
|
|
|
|
_logger.warning("Template context keys: %s", list(template_context.keys()))
|
|
|
|
return request.render(
|
|
"website_sale_aplicoop.eskaera_checkout", template_context
|
|
)
|
|
|
|
@http.route(
|
|
["/eskaera/save-cart"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
methods=["POST"],
|
|
csrf=False,
|
|
)
|
|
def save_cart_draft(self, **post):
|
|
"""Save cart items as a draft sale.order with pickup date."""
|
|
import json
|
|
|
|
try:
|
|
_logger.warning("=== SAVE_CART_DRAFT CALLED ===")
|
|
|
|
if not request.httprequest.data:
|
|
return request.make_response(
|
|
json.dumps({"error": "No data provided"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Decode JSON
|
|
try:
|
|
raw_data = request.httprequest.data
|
|
if isinstance(raw_data, bytes):
|
|
raw_data = raw_data.decode("utf-8")
|
|
data = json.loads(raw_data)
|
|
except Exception as e:
|
|
_logger.error("Error decoding JSON: %s", str(e))
|
|
return request.make_response(
|
|
json.dumps({"error": f"Invalid JSON: {str(e)}"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
_logger.info("save_cart_draft data received: %s", data)
|
|
|
|
# Validate order_id
|
|
order_id = data.get("order_id")
|
|
if not order_id:
|
|
return request.make_response(
|
|
json.dumps({"error": "order_id is required"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
try:
|
|
order_id = int(order_id)
|
|
except (ValueError, TypeError):
|
|
return request.make_response(
|
|
json.dumps({"error": f"Invalid order_id format: {order_id}"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Verify that the order exists
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
if not group_order.exists():
|
|
return request.make_response(
|
|
json.dumps({"error": f"Order {order_id} not found"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
current_user = request.env.user
|
|
if not current_user.partner_id:
|
|
return request.make_response(
|
|
json.dumps({"error": "User has no associated partner"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Get cart items and pickup date
|
|
items = data.get("items", [])
|
|
pickup_date = data.get("pickup_date") # Date from group_order
|
|
|
|
if not items:
|
|
return request.make_response(
|
|
json.dumps({"error": "No items in cart"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
_logger.info(
|
|
"Creating draft sale.order with %d items for partner %d",
|
|
len(items),
|
|
current_user.partner_id.id,
|
|
)
|
|
|
|
# Create sales.order lines from items
|
|
sale_order_lines = []
|
|
for item in items:
|
|
try:
|
|
product_id = int(item.get("product_id"))
|
|
quantity = float(item.get("quantity", 1))
|
|
price = float(item.get("product_price", 0))
|
|
|
|
product = request.env["product.product"].browse(product_id)
|
|
if not product.exists():
|
|
_logger.warning(
|
|
"save_cart_draft: Product %d does not exist", product_id
|
|
)
|
|
continue
|
|
|
|
line = (
|
|
0,
|
|
0,
|
|
{
|
|
"product_id": product_id,
|
|
"product_uom_qty": quantity,
|
|
"price_unit": price,
|
|
},
|
|
)
|
|
sale_order_lines.append(line)
|
|
|
|
except Exception as e:
|
|
_logger.error("Error processing item %s: %s", item, str(e))
|
|
|
|
if not sale_order_lines:
|
|
return request.make_response(
|
|
json.dumps({"error": "No valid items to save"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Create order values dict
|
|
order_vals = {
|
|
"partner_id": current_user.partner_id.id,
|
|
"order_line": sale_order_lines,
|
|
"state": "draft",
|
|
"group_order_id": order_id, # Link to the group.order
|
|
}
|
|
|
|
# Propagate fields from group order (ensure they exist)
|
|
if group_order.pickup_day:
|
|
order_vals["pickup_day"] = group_order.pickup_day
|
|
_logger.info("Set pickup_day: %s", group_order.pickup_day)
|
|
|
|
if group_order.pickup_date:
|
|
order_vals["pickup_date"] = group_order.pickup_date
|
|
_logger.info("Set pickup_date: %s", group_order.pickup_date)
|
|
|
|
if group_order.home_delivery:
|
|
order_vals["home_delivery"] = group_order.home_delivery
|
|
_logger.info("Set home_delivery: %s", group_order.home_delivery)
|
|
|
|
# Add commitment date (pickup/delivery date) if provided
|
|
if pickup_date:
|
|
order_vals["commitment_date"] = pickup_date
|
|
elif group_order.pickup_date:
|
|
# Fallback to group order pickup date
|
|
order_vals["commitment_date"] = group_order.pickup_date
|
|
_logger.info(
|
|
"Set commitment_date from group_order.pickup_date: %s",
|
|
group_order.pickup_date,
|
|
)
|
|
|
|
_logger.info("Creating sale.order with values: %s", order_vals)
|
|
|
|
# Create the sale.order
|
|
sale_order = request.env["sale.order"].create(order_vals)
|
|
|
|
# Ensure the order has a name (draft orders may not have one yet)
|
|
if not sale_order.name or sale_order.name == "New":
|
|
# Force sequence generation for draft order
|
|
sale_order._onchange_partner_id() # This may trigger name generation
|
|
if not sale_order.name or sale_order.name == "New":
|
|
# If still no name, use a temporary one
|
|
sale_order.name = "DRAFT-%s" % sale_order.id
|
|
|
|
_logger.info(
|
|
"Draft sale.order created: %d (name: %s) for partner %d with group_order_id=%s, pickup_day=%s, pickup_date=%s",
|
|
sale_order.id,
|
|
sale_order.name,
|
|
current_user.partner_id.id,
|
|
sale_order.group_order_id.id if sale_order.group_order_id else None,
|
|
sale_order.pickup_day,
|
|
sale_order.pickup_date,
|
|
)
|
|
|
|
return request.make_response(
|
|
json.dumps(
|
|
{
|
|
"success": True,
|
|
"message": _("Cart saved as draft"),
|
|
"sale_order_id": sale_order.id,
|
|
}
|
|
),
|
|
[("Content-Type", "application/json")],
|
|
)
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
|
|
_logger.error("save_cart_draft: Unexpected error: %s", str(e))
|
|
_logger.error(traceback.format_exc())
|
|
return request.make_response(
|
|
json.dumps({"error": str(e)}),
|
|
[("Content-Type", "application/json")],
|
|
status=500,
|
|
)
|
|
|
|
@http.route(
|
|
["/eskaera/load-draft"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
methods=["POST"],
|
|
csrf=False,
|
|
)
|
|
def load_draft_cart(self, **post):
|
|
"""Load items from the most recent draft sale.order for this week."""
|
|
import json
|
|
from datetime import datetime
|
|
from datetime import timedelta
|
|
|
|
try:
|
|
_logger.warning("=== LOAD_DRAFT_CART CALLED ===")
|
|
|
|
if not request.httprequest.data:
|
|
return request.make_response(
|
|
json.dumps({"error": "No data provided"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Decode JSON
|
|
try:
|
|
raw_data = request.httprequest.data
|
|
if isinstance(raw_data, bytes):
|
|
raw_data = raw_data.decode("utf-8")
|
|
data = json.loads(raw_data)
|
|
except Exception as e:
|
|
_logger.error("Error decoding JSON: %s", str(e))
|
|
return request.make_response(
|
|
json.dumps({"error": f"Invalid JSON: {str(e)}"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
order_id = data.get("order_id")
|
|
if not order_id:
|
|
return request.make_response(
|
|
json.dumps({"error": "order_id is required"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
try:
|
|
order_id = int(order_id)
|
|
except (ValueError, TypeError):
|
|
return request.make_response(
|
|
json.dumps({"error": f"Invalid order_id format: {order_id}"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
if not group_order.exists():
|
|
return request.make_response(
|
|
json.dumps({"error": f"Order {order_id} not found"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
current_user = request.env.user
|
|
if not current_user.partner_id:
|
|
return request.make_response(
|
|
json.dumps({"error": "User has no associated partner"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Find the most recent draft sale.order for this partner from this week
|
|
# Get start of current week (Monday)
|
|
today = datetime.now().date()
|
|
start_of_week = today - timedelta(days=today.weekday())
|
|
end_of_week = start_of_week + timedelta(days=6)
|
|
|
|
_logger.info(
|
|
"Searching for draft orders between %s and %s for partner %d and group_order %d",
|
|
start_of_week,
|
|
end_of_week,
|
|
current_user.partner_id.id,
|
|
order_id,
|
|
)
|
|
|
|
# Debug: Check all draft orders for this user
|
|
all_drafts = request.env["sale.order"].search(
|
|
[
|
|
("partner_id", "=", current_user.partner_id.id),
|
|
("state", "=", "draft"),
|
|
]
|
|
)
|
|
_logger.info(
|
|
"DEBUG: Found %d total draft orders for partner %d:",
|
|
len(all_drafts),
|
|
current_user.partner_id.id,
|
|
)
|
|
for draft in all_drafts:
|
|
_logger.info(
|
|
" - Order ID: %d, group_order_id: %s, create_date: %s",
|
|
draft.id,
|
|
draft.group_order_id.id if draft.group_order_id else "None",
|
|
draft.create_date,
|
|
)
|
|
|
|
draft_orders = request.env["sale.order"].search(
|
|
[
|
|
("partner_id", "=", current_user.partner_id.id),
|
|
("group_order_id", "=", order_id), # Filter by group.order
|
|
("state", "=", "draft"),
|
|
("create_date", ">=", f"{start_of_week} 00:00:00"),
|
|
("create_date", "<=", f"{end_of_week} 23:59:59"),
|
|
],
|
|
order="create_date desc",
|
|
limit=1,
|
|
)
|
|
|
|
_logger.info(
|
|
"DEBUG: Found %d matching draft orders with filters", len(draft_orders)
|
|
)
|
|
|
|
if not draft_orders:
|
|
error_msg = request.env._("No draft orders found for this week")
|
|
return request.make_response(
|
|
json.dumps({"error": error_msg}),
|
|
[("Content-Type", "application/json")],
|
|
status=404,
|
|
)
|
|
|
|
draft_order = draft_orders[0]
|
|
|
|
# Extract items from the draft order
|
|
items = []
|
|
for line in draft_order.order_line:
|
|
items.append(
|
|
{
|
|
"product_id": line.product_id.id,
|
|
"product_name": line.product_id.name,
|
|
"quantity": line.product_uom_qty,
|
|
"product_price": line.price_unit,
|
|
}
|
|
)
|
|
|
|
_logger.info(
|
|
"Loaded %d items from draft order %d", len(items), draft_order.id
|
|
)
|
|
|
|
return request.make_response(
|
|
json.dumps(
|
|
{
|
|
"success": True,
|
|
"message": _("Draft order loaded"),
|
|
"items": items,
|
|
"sale_order_id": draft_order.id,
|
|
"group_order_id": draft_order.group_order_id.id,
|
|
"group_order_name": draft_order.group_order_id.name,
|
|
"pickup_day": draft_order.pickup_day,
|
|
"pickup_date": (
|
|
str(draft_order.pickup_date)
|
|
if draft_order.pickup_date
|
|
else None
|
|
),
|
|
"home_delivery": draft_order.home_delivery,
|
|
}
|
|
),
|
|
[("Content-Type", "application/json")],
|
|
)
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
|
|
_logger.error("load_draft_cart: Unexpected error: %s", str(e))
|
|
_logger.error(traceback.format_exc())
|
|
return request.make_response(
|
|
json.dumps({"error": str(e)}),
|
|
[("Content-Type", "application/json")],
|
|
status=500,
|
|
)
|
|
|
|
@http.route(
|
|
["/eskaera/save-order"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
methods=["POST"],
|
|
csrf=False,
|
|
)
|
|
def save_eskaera_draft(self, **post):
|
|
"""Save order as draft (without confirming).
|
|
|
|
Creates a sale.order from the cart items with state='draft'.
|
|
If a draft already exists for this group order, prompt user for merge/replace.
|
|
"""
|
|
import json
|
|
|
|
try:
|
|
_logger.warning("=== SAVE_ESKAERA_DRAFT CALLED ===")
|
|
|
|
if not request.httprequest.data:
|
|
_logger.warning("save_eskaera_draft: No request data provided")
|
|
return request.make_response(
|
|
json.dumps({"error": "No data provided"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Decode JSON
|
|
try:
|
|
raw_data = request.httprequest.data
|
|
if isinstance(raw_data, bytes):
|
|
raw_data = raw_data.decode("utf-8")
|
|
data = json.loads(raw_data)
|
|
except Exception as e:
|
|
_logger.error("Error decoding JSON: %s", str(e))
|
|
return request.make_response(
|
|
json.dumps({"error": f"Invalid JSON: {str(e)}"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
_logger.info("save_eskaera_draft data received: %s", data)
|
|
|
|
# Validate order_id
|
|
order_id = data.get("order_id")
|
|
if not order_id:
|
|
_logger.warning("save_eskaera_draft: order_id missing")
|
|
return request.make_response(
|
|
json.dumps({"error": "order_id is required"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Convert to int
|
|
try:
|
|
order_id = int(order_id)
|
|
except (ValueError, TypeError):
|
|
_logger.warning("save_eskaera_draft: Invalid order_id: %s", order_id)
|
|
return request.make_response(
|
|
json.dumps({"error": f"Invalid order_id format: {order_id}"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Verify that the order exists
|
|
group_order = request.env["group.order"].browse(order_id)
|
|
if not group_order.exists():
|
|
_logger.warning("save_eskaera_draft: Order %d not found", order_id)
|
|
return request.make_response(
|
|
json.dumps({"error": f"Order {order_id} not found"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
current_user = request.env.user
|
|
|
|
# Validate that the user has a partner_id
|
|
if not current_user.partner_id:
|
|
_logger.error(
|
|
"save_eskaera_draft: User %d has no partner_id", current_user.id
|
|
)
|
|
return request.make_response(
|
|
json.dumps({"error": "User has no associated partner"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Get cart items
|
|
items = data.get("items", [])
|
|
merge_action = data.get("merge_action") # 'merge' or 'replace'
|
|
existing_draft_id = data.get("existing_draft_id") # ID if replacing
|
|
|
|
if not items:
|
|
_logger.warning(
|
|
"save_eskaera_draft: No items in cart for user %d in order %d",
|
|
current_user.id,
|
|
order_id,
|
|
)
|
|
return request.make_response(
|
|
json.dumps({"error": "No items in cart"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Check if a draft already exists for this group order and user
|
|
existing_drafts = request.env["sale.order"].search(
|
|
[
|
|
("group_order_id", "=", order_id),
|
|
("partner_id", "=", current_user.partner_id.id),
|
|
("state", "=", "draft"),
|
|
]
|
|
)
|
|
|
|
# If draft exists and no action specified, return the existing draft info
|
|
if existing_drafts and not merge_action:
|
|
existing_draft = existing_drafts[0] # Get first draft
|
|
existing_items = [
|
|
{
|
|
"product_id": line.product_id.id,
|
|
"product_name": line.product_id.name,
|
|
"quantity": line.product_uom_qty,
|
|
"product_price": line.price_unit,
|
|
}
|
|
for line in existing_draft.order_line
|
|
]
|
|
|
|
return request.make_response(
|
|
json.dumps(
|
|
{
|
|
"success": False,
|
|
"existing_draft": True,
|
|
"existing_draft_id": existing_draft.id,
|
|
"existing_items": existing_items,
|
|
"current_items": items,
|
|
"message": _("A draft already exists for this week."),
|
|
}
|
|
),
|
|
[("Content-Type", "application/json")],
|
|
)
|
|
|
|
_logger.info(
|
|
"Creating draft sale.order with %d items for partner %d",
|
|
len(items),
|
|
current_user.partner_id.id,
|
|
)
|
|
|
|
# Create sales.order lines from items
|
|
sale_order_lines = []
|
|
for item in items:
|
|
try:
|
|
product_id = int(item.get("product_id"))
|
|
quantity = float(item.get("quantity", 1))
|
|
price = float(item.get("product_price", 0))
|
|
|
|
product = request.env["product.product"].browse(product_id)
|
|
if not product.exists():
|
|
_logger.warning(
|
|
"save_eskaera_draft: Product %d does not exist", product_id
|
|
)
|
|
continue
|
|
|
|
# Calculate subtotal
|
|
subtotal = quantity * price
|
|
_logger.info(
|
|
"Item: product_id=%d, quantity=%.2f, price=%.2f, subtotal=%.2f",
|
|
product_id,
|
|
quantity,
|
|
price,
|
|
subtotal,
|
|
)
|
|
|
|
# Create order line as a tuple for create() operation
|
|
line = (
|
|
0,
|
|
0,
|
|
{
|
|
"product_id": product_id,
|
|
"product_uom_qty": quantity,
|
|
"price_unit": price,
|
|
},
|
|
)
|
|
sale_order_lines.append(line)
|
|
|
|
except Exception as e:
|
|
_logger.error("Error processing item %s: %s", item, str(e))
|
|
|
|
if not sale_order_lines:
|
|
return request.make_response(
|
|
json.dumps({"error": "No valid items to save"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Handle merge vs replace action
|
|
if merge_action == "merge" and existing_draft_id:
|
|
# Merge: Add items to existing draft
|
|
existing_draft = request.env["sale.order"].browse(
|
|
int(existing_draft_id)
|
|
)
|
|
if existing_draft.exists():
|
|
# Merge items: update quantities if product exists, add if new
|
|
for new_line_data in sale_order_lines:
|
|
product_id = new_line_data[2]["product_id"]
|
|
new_quantity = new_line_data[2]["product_uom_qty"]
|
|
new_price = new_line_data[2]["price_unit"]
|
|
|
|
# Find if product already exists in draft
|
|
existing_line = existing_draft.order_line.filtered(
|
|
lambda line: line.product_id.id == product_id
|
|
)
|
|
|
|
if existing_line:
|
|
# Update quantity (add to existing)
|
|
existing_line.write(
|
|
{
|
|
"product_uom_qty": existing_line.product_uom_qty
|
|
+ new_quantity,
|
|
}
|
|
)
|
|
_logger.info(
|
|
"Merged item: product_id=%d, new total quantity=%.2f",
|
|
product_id,
|
|
existing_line.product_uom_qty,
|
|
)
|
|
else:
|
|
# Add new line to existing draft
|
|
existing_draft.order_line.create(
|
|
{
|
|
"order_id": existing_draft.id,
|
|
"product_id": product_id,
|
|
"product_uom_qty": new_quantity,
|
|
"price_unit": new_price,
|
|
}
|
|
)
|
|
_logger.info(
|
|
"Added new item to draft: product_id=%d, quantity=%.2f",
|
|
product_id,
|
|
new_quantity,
|
|
)
|
|
|
|
sale_order = existing_draft
|
|
merge_success = True
|
|
|
|
elif merge_action == "replace" and existing_draft_id and existing_drafts:
|
|
# Replace: Delete old draft and create new one
|
|
existing_drafts.unlink()
|
|
_logger.info("Deleted existing draft %s", existing_draft_id)
|
|
|
|
# Create new draft with current items
|
|
sale_order = request.env["sale.order"].create(
|
|
{
|
|
"partner_id": current_user.partner_id.id,
|
|
"order_line": sale_order_lines,
|
|
"state": "draft", # Explicitly set to draft
|
|
"group_order_id": order_id, # Link to the group.order
|
|
"pickup_day": group_order.pickup_day, # Propagate from group order
|
|
"pickup_date": group_order.pickup_date, # Propagate from group order
|
|
"home_delivery": group_order.home_delivery, # Propagate from group order
|
|
}
|
|
)
|
|
merge_success = False
|
|
|
|
else:
|
|
# No existing draft, create new one
|
|
sale_order = request.env["sale.order"].create(
|
|
{
|
|
"partner_id": current_user.partner_id.id,
|
|
"order_line": sale_order_lines,
|
|
"state": "draft", # Explicitly set to draft
|
|
"group_order_id": order_id, # Link to the group.order
|
|
"pickup_day": group_order.pickup_day, # Propagate from group order
|
|
"pickup_date": group_order.pickup_date, # Propagate from group order
|
|
"home_delivery": group_order.home_delivery, # Propagate from group order
|
|
}
|
|
)
|
|
merge_success = False
|
|
|
|
_logger.info(
|
|
"Draft sale.order created/updated: %d for partner %d with group_order_id=%s, pickup_day=%s, pickup_date=%s, home_delivery=%s",
|
|
sale_order.id,
|
|
current_user.partner_id.id,
|
|
sale_order.group_order_id.id if sale_order.group_order_id else None,
|
|
sale_order.pickup_day,
|
|
sale_order.pickup_date,
|
|
sale_order.home_delivery,
|
|
)
|
|
|
|
return request.make_response(
|
|
json.dumps(
|
|
{
|
|
"success": True,
|
|
"message": (
|
|
_("Merged with existing draft")
|
|
if merge_success
|
|
else _("Order saved as draft")
|
|
),
|
|
"sale_order_id": sale_order.id,
|
|
"merged": merge_success,
|
|
}
|
|
),
|
|
[("Content-Type", "application/json")],
|
|
)
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
|
|
_logger.error("save_eskaera_draft: Unexpected error: %s", str(e))
|
|
_logger.error(traceback.format_exc())
|
|
return request.make_response(
|
|
json.dumps({"error": str(e)}),
|
|
[("Content-Type", "application/json")],
|
|
status=500,
|
|
)
|
|
|
|
@http.route(
|
|
["/eskaera/confirm"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
methods=["POST"],
|
|
csrf=False,
|
|
)
|
|
def confirm_eskaera(self, **post):
|
|
"""Confirm order and create sale.order from cart (localStorage).
|
|
|
|
Items come from the cart stored in the frontend localStorage.
|
|
"""
|
|
import json
|
|
|
|
try:
|
|
# Initial log for debug
|
|
_logger.warning("=== CONFIRM_ESKAERA CALLED ===")
|
|
_logger.warning(
|
|
"Request data: %s",
|
|
request.httprequest.data[:200] if request.httprequest.data else "EMPTY",
|
|
)
|
|
|
|
# Get JSON data from the request body
|
|
if not request.httprequest.data:
|
|
_logger.warning("confirm_eskaera: No request data provided")
|
|
return request.make_response(
|
|
json.dumps({"error": "No data provided"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# Decode JSON
|
|
try:
|
|
raw_data = request.httprequest.data
|
|
if isinstance(raw_data, bytes):
|
|
raw_data = raw_data.decode("utf-8")
|
|
data = json.loads(raw_data)
|
|
except Exception as e:
|
|
_logger.error("Error decoding JSON: %s", str(e))
|
|
return request.make_response(
|
|
json.dumps({"error": f"Invalid JSON: {str(e)}"}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
_logger.info("confirm_eskaera data received: %s", data)
|
|
|
|
# Validate request using helper
|
|
try:
|
|
(
|
|
order_id,
|
|
group_order,
|
|
current_user,
|
|
items,
|
|
is_delivery,
|
|
) = self._validate_confirm_json(data)
|
|
except ValueError as e:
|
|
_logger.warning("confirm_eskaera: Validation error: %s", str(e))
|
|
return request.make_response(
|
|
json.dumps({"error": str(e)}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
_logger.info("Current user: %d", current_user.id)
|
|
|
|
# Process cart items using helper
|
|
try:
|
|
sale_order_lines = self._process_cart_items(items, group_order)
|
|
except ValueError as e:
|
|
_logger.warning("confirm_eskaera: Cart processing error: %s", str(e))
|
|
return request.make_response(
|
|
json.dumps({"error": str(e)}),
|
|
[("Content-Type", "application/json")],
|
|
status=400,
|
|
)
|
|
|
|
# First, check if there's already a draft sale.order for this user in this group order
|
|
existing_order = request.env["sale.order"].search(
|
|
[
|
|
("partner_id", "=", current_user.partner_id.id),
|
|
("group_order_id", "=", group_order.id),
|
|
("state", "=", "draft"),
|
|
],
|
|
limit=1,
|
|
)
|
|
|
|
if existing_order:
|
|
_logger.info(
|
|
"Found existing draft order: %d, updating instead of creating new",
|
|
existing_order.id,
|
|
)
|
|
# Delete existing lines and create new ones
|
|
existing_order.order_line.unlink()
|
|
sale_order = existing_order
|
|
else:
|
|
_logger.info(
|
|
"No existing draft order found, will create new sale.order"
|
|
)
|
|
sale_order = None
|
|
|
|
# Get pickup date and delivery info from group order
|
|
# If delivery, use delivery_date; otherwise use pickup_date
|
|
commitment_date = None
|
|
if is_delivery and group_order.delivery_date:
|
|
commitment_date = group_order.delivery_date.isoformat()
|
|
elif group_order.pickup_date:
|
|
commitment_date = group_order.pickup_date.isoformat()
|
|
|
|
# Create or update sale.order
|
|
try:
|
|
if sale_order:
|
|
# Update existing order with new lines
|
|
_logger.info(
|
|
"Updating existing sale.order %d with %d items",
|
|
sale_order.id,
|
|
len(sale_order_lines),
|
|
)
|
|
sale_order.order_line = sale_order_lines
|
|
# Ensure group_order_id is set and propagate group order fields
|
|
if not sale_order.group_order_id:
|
|
sale_order.group_order_id = group_order.id
|
|
# Propagate pickup day, date, and delivery type from group order
|
|
sale_order.pickup_day = group_order.pickup_day
|
|
sale_order.pickup_date = group_order.pickup_date
|
|
sale_order.home_delivery = is_delivery
|
|
if commitment_date:
|
|
sale_order.commitment_date = commitment_date
|
|
_logger.info(
|
|
"Updated sale.order %d: commitment_date=%s, home_delivery=%s",
|
|
sale_order.id,
|
|
commitment_date,
|
|
is_delivery,
|
|
)
|
|
else:
|
|
# Create new order
|
|
order_vals = {
|
|
"partner_id": current_user.partner_id.id,
|
|
"order_line": sale_order_lines,
|
|
"group_order_id": group_order.id,
|
|
"pickup_day": group_order.pickup_day,
|
|
"pickup_date": group_order.pickup_date,
|
|
"home_delivery": is_delivery,
|
|
}
|
|
|
|
# Add commitment date (pickup/delivery date) if available
|
|
if commitment_date:
|
|
order_vals["commitment_date"] = commitment_date
|
|
|
|
sale_order = request.env["sale.order"].create(order_vals)
|
|
_logger.info(
|
|
"sale.order created successfully: %d with group_order_id=%d, pickup_day=%s, home_delivery=%s",
|
|
sale_order.id,
|
|
group_order.id,
|
|
group_order.pickup_day,
|
|
group_order.home_delivery,
|
|
)
|
|
except Exception as e:
|
|
_logger.error("Error creating/updating sale.order: %s", str(e))
|
|
_logger.error("sale_order_lines: %s", sale_order_lines)
|
|
raise
|
|
|
|
# Build confirmation message using helper
|
|
message_data = self._build_confirmation_message(
|
|
sale_order, group_order, is_delivery
|
|
)
|
|
message = message_data["message"]
|
|
pickup_day_name = message_data["pickup_day"]
|
|
pickup_date_str = message_data["pickup_date"]
|
|
pickup_day_index = message_data["pickup_day_index"]
|
|
|
|
response_data = {
|
|
"success": True,
|
|
"message": message,
|
|
"sale_order_id": sale_order.id,
|
|
"redirect_url": sale_order.get_portal_url(),
|
|
"group_order_name": group_order.name,
|
|
"pickup_day": pickup_day_name,
|
|
"pickup_date": pickup_date_str,
|
|
"pickup_day_index": pickup_day_index,
|
|
}
|
|
|
|
# Log language and final message to debug translation issues
|
|
try:
|
|
_logger.info(
|
|
'confirm_eskaera: lang=%s, message="%s"', request.env.lang, message
|
|
)
|
|
except Exception:
|
|
_logger.info("confirm_eskaera: message logging failed")
|
|
|
|
_logger.info(
|
|
"Order %d confirmed successfully, sale.order created: %d",
|
|
order_id,
|
|
sale_order.id,
|
|
)
|
|
|
|
# Confirm the sale.order (change state from draft to sale)
|
|
try:
|
|
sale_order.action_confirm()
|
|
_logger.info(
|
|
"sale.order %d confirmed (state changed to sale)", sale_order.id
|
|
)
|
|
except Exception as e:
|
|
_logger.warning(
|
|
"Failed to confirm sale.order %d: %s", sale_order.id, str(e)
|
|
)
|
|
# Continue anyway, the order was created/updated
|
|
|
|
return request.make_response(
|
|
json.dumps(response_data), [("Content-Type", "application/json")]
|
|
)
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
|
|
_logger.error("confirm_eskaera: Unexpected error: %s", str(e))
|
|
_logger.error(traceback.format_exc())
|
|
return request.make_response(
|
|
json.dumps({"error": str(e)}),
|
|
[("Content-Type", "application/json")],
|
|
status=500,
|
|
)
|
|
|
|
@http.route(
|
|
["/eskaera/<int:group_order_id>/load-from-history/<int:sale_order_id>"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
)
|
|
def load_order_from_history(self, group_order_id=None, sale_order_id=None, **post):
|
|
"""Load a historical order (draft/confirmed) back into the cart.
|
|
|
|
Used by portal "Load in Cart" button on My Orders page.
|
|
Extracts items from the order and redirects to the group order page,
|
|
where the JavaScript auto-load will populate the cart.
|
|
"""
|
|
try:
|
|
# Get the sale.order record
|
|
sale_order = request.env["sale.order"].browse(sale_order_id)
|
|
if not sale_order.exists():
|
|
return request.redirect("/shop")
|
|
|
|
# Verify this order belongs to current user
|
|
current_partner = request.env.user.partner_id
|
|
if sale_order.partner_id.id != current_partner.id:
|
|
_logger.warning(
|
|
"User %s attempted to load order %d belonging to partner %d",
|
|
request.env.user.login,
|
|
sale_order_id,
|
|
sale_order.partner_id.id,
|
|
)
|
|
return request.redirect("/shop")
|
|
|
|
# Verify the order belongs to the requested group_order
|
|
if sale_order.group_order_id.id != group_order_id:
|
|
return request.redirect("/eskaera/%d" % sale_order.group_order_id.id)
|
|
|
|
# Extract items from the order (skip delivery product)
|
|
delivery_product = request.env.ref(
|
|
"website_sale_aplicoop.product_home_delivery", raise_if_not_found=False
|
|
)
|
|
delivery_product_id = delivery_product.id if delivery_product else None
|
|
|
|
items = []
|
|
for line in sale_order.order_line:
|
|
# Skip the delivery product
|
|
if delivery_product_id and line.product_id.id == delivery_product_id:
|
|
continue
|
|
|
|
items.append(
|
|
{
|
|
"product_id": line.product_id.id,
|
|
"product_name": line.product_id.name,
|
|
"quantity": line.product_uom_qty,
|
|
"price": line.price_unit, # Unit price
|
|
}
|
|
)
|
|
|
|
# Store items in localStorage by passing via URL parameter or session
|
|
# We'll use sessionStorage in JavaScript to avoid URL length limits
|
|
|
|
# Check if the order being loaded is from the same group order
|
|
# If not, don't restore the old pickup fields - use the current group order's fields
|
|
same_group_order = sale_order.group_order_id.id == group_order_id
|
|
|
|
# If loading from same group order, restore old pickup fields
|
|
# Otherwise, page will show current group order's pickup fields
|
|
pickup_day_to_restore = sale_order.pickup_day if same_group_order else None
|
|
pickup_date_to_restore = (
|
|
str(sale_order.pickup_date)
|
|
if (same_group_order and sale_order.pickup_date)
|
|
else None
|
|
)
|
|
home_delivery_to_restore = (
|
|
sale_order.home_delivery if same_group_order else None
|
|
)
|
|
|
|
response = request.make_response(
|
|
request.render(
|
|
"website_sale_aplicoop.eskaera_load_from_history",
|
|
{
|
|
"group_order_id": group_order_id,
|
|
"items_json": json.dumps(items), # Pass serialized JSON
|
|
"sale_order": sale_order,
|
|
"sale_order_name": sale_order.name, # Pass order reference
|
|
"pickup_day": pickup_day_to_restore, # Pass pickup day (or None if different group)
|
|
"pickup_date": pickup_date_to_restore, # Pass pickup date (or None if different group)
|
|
"home_delivery": home_delivery_to_restore, # Pass home delivery flag (or None if different group)
|
|
"same_group_order": same_group_order, # Indicate if from same group order
|
|
},
|
|
),
|
|
)
|
|
return response
|
|
|
|
except Exception as e:
|
|
_logger.error("load_order_from_history: %s", str(e))
|
|
import traceback
|
|
|
|
_logger.error(traceback.format_exc())
|
|
return request.redirect("/eskaera/%d" % group_order_id)
|
|
|
|
@http.route(
|
|
["/eskaera/<int:group_order_id>/confirm/<int:sale_order_id>"],
|
|
type="json",
|
|
auth="user",
|
|
website=True,
|
|
methods=["POST"],
|
|
)
|
|
def confirm_order_from_portal(
|
|
self, group_order_id=None, sale_order_id=None, **post
|
|
):
|
|
"""Confirm a draft order from the portal (AJAX endpoint).
|
|
|
|
Used by portal "Confirm" button on My Orders page.
|
|
Confirms the draft order and returns JSON response.
|
|
Does NOT redirect - the calling JavaScript handles the response.
|
|
"""
|
|
_logger.info(
|
|
"confirm_order_from_portal called: group_order_id=%s, sale_order_id=%s",
|
|
group_order_id,
|
|
sale_order_id,
|
|
)
|
|
|
|
try:
|
|
# Get the sale.order record
|
|
sale_order = request.env["sale.order"].browse(sale_order_id)
|
|
if not sale_order.exists():
|
|
_logger.warning(
|
|
"confirm_order_from_portal: Order %d not found", sale_order_id
|
|
)
|
|
return {"success": False, "error": "Order not found"}
|
|
|
|
# Verify this order belongs to current user
|
|
current_partner = request.env.user.partner_id
|
|
if sale_order.partner_id.id != current_partner.id:
|
|
_logger.warning(
|
|
"User %s attempted to confirm order %d belonging to partner %d",
|
|
request.env.user.login,
|
|
sale_order_id,
|
|
sale_order.partner_id.id,
|
|
)
|
|
return {"success": False, "error": "Unauthorized"}
|
|
|
|
# Verify the order belongs to the requested group_order
|
|
if sale_order.group_order_id.id != group_order_id:
|
|
_logger.warning(
|
|
"Order %d belongs to group %d, not %d",
|
|
sale_order_id,
|
|
sale_order.group_order_id.id,
|
|
group_order_id,
|
|
)
|
|
return {
|
|
"success": False,
|
|
"error": f"Order belongs to different group: {sale_order.group_order_id.id}",
|
|
}
|
|
|
|
# Only allow confirming draft orders
|
|
if sale_order.state != "draft":
|
|
_logger.warning(
|
|
"Order %d is in state %s, not draft",
|
|
sale_order_id,
|
|
sale_order.state,
|
|
)
|
|
return {
|
|
"success": False,
|
|
"error": f"Order is already {sale_order.state}, cannot confirm again",
|
|
}
|
|
|
|
# Confirm the order (change state to 'sale')
|
|
sale_order.action_confirm()
|
|
_logger.info(
|
|
"Order %d confirmed from portal by user %s",
|
|
sale_order_id,
|
|
request.env.user.login,
|
|
)
|
|
|
|
# Return success response with updated order state
|
|
return {
|
|
"success": True,
|
|
"message": _("Order confirmed successfully"),
|
|
"order_id": sale_order_id,
|
|
"order_state": sale_order.state,
|
|
"group_order_id": group_order_id,
|
|
}
|
|
|
|
except Exception as e:
|
|
_logger.error("confirm_order_from_portal: %s", str(e))
|
|
import traceback
|
|
|
|
_logger.error(traceback.format_exc())
|
|
return {"success": False, "error": f"Error confirming order: {str(e)}"}
|
|
|
|
def _translate_labels(self, labels_dict, lang):
|
|
"""Manually translate labels based on user language.
|
|
|
|
This is a fallback translation method for when Odoo's translation system
|
|
hasn't loaded translations from .po files properly.
|
|
"""
|
|
translations = {
|
|
"es_ES": {
|
|
"Draft Already Exists": "El Borrador Ya Existe",
|
|
"A saved draft already exists for this week.": "Un borrador guardado ya existe para esta semana.",
|
|
"You have two options:": "Tienes dos opciones:",
|
|
"Option 1: Merge with Existing Draft": "Opción 1: Fusionar con Borrador Existente",
|
|
"Combine your current cart with the existing draft.": "Combina tu carrito actual con el borrador existente.",
|
|
"Existing draft has": "El borrador existente tiene",
|
|
"Current cart has": "Tu carrito actual tiene",
|
|
"item(s)": "artículo(s)",
|
|
"Products will be merged by adding quantities. If a product exists in both, quantities will be combined.": "Los productos se fusionarán sumando cantidades. Si un producto existe en ambos, las cantidades se combinarán.",
|
|
"Option 2: Replace with Current Cart": "Opción 2: Reemplazar con Carrito Actual",
|
|
"Delete the old draft and save only the current cart items.": "Elimina el borrador anterior y guarda solo los artículos del carrito actual.",
|
|
"The existing draft will be permanently deleted.": "El borrador existente se eliminará permanentemente.",
|
|
"Merge": "Fusionar",
|
|
"Replace": "Reemplazar",
|
|
"Cancel": "Cancelar",
|
|
# Checkout page labels
|
|
"Home Delivery": "Entrega a Domicilio",
|
|
"Delivery Information": "Información de Entrega",
|
|
"Your order will be delivered the day after pickup between 11:00 - 14:00": "Tu pedido será entregado el día después de la recogida entre las 11:00 - 14:00",
|
|
"Important": "Importante",
|
|
"Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.": "Una vez confirmes este pedido, no podrás modificarlo. Por favor, revisa cuidadosamente antes de confirmar.",
|
|
},
|
|
"eu_ES": {
|
|
"Draft Already Exists": "Zirriborro Dagoeneko Badago",
|
|
"A saved draft already exists for this week.": "Gordetako zirriborro bat dagoeneko badago asteburu honetarako.",
|
|
"You have two options:": "Bi aukera dituzu:",
|
|
"Option 1: Merge with Existing Draft": "1. Aukera: Existentea Duen Zirriborroarekin Batu",
|
|
"Combine your current cart with the existing draft.": "Batu zure gaur-oraingo saskia existentea duen zirriborroarekin.",
|
|
"Existing draft has": "Existentea duen zirriborroak du",
|
|
"Current cart has": "Zure gaur-oraingo saskiak du",
|
|
"item(s)": "artikulu(a)",
|
|
"Products will be merged by adding quantities. If a product exists in both, quantities will be combined.": "Produktuak batuko dira kantitateak gehituz. Produktu bat bian badago, kantitateak konbinatuko dira.",
|
|
"Option 2: Replace with Current Cart": "2. Aukera: Gaur-oraingo Askiarekin Ordeztu",
|
|
"Delete the old draft and save only the current cart items.": "Ezabatu zahar-zirriborroa eta gorde soilik gaur-oraingo saskiaren artikulua.",
|
|
"The existing draft will be permanently deleted.": "Existentea duen zirriborroa behin betiko ezabatuko da.",
|
|
"Merge": "Batu",
|
|
"Replace": "Ordeztu",
|
|
"Cancel": "Ezeztatu",
|
|
# Checkout page labels
|
|
"Home Delivery": "Etxera Bidalketa",
|
|
"Delivery Information": "Bidalketaren Informazioa",
|
|
"Your order will be delivered the day after pickup between 11:00 - 14:00": "Zure eskaera bidaliko da biltzeko eguaren ondoren 11:00 - 14:00 bitartean",
|
|
"Important": "Garrantzitsua",
|
|
"Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.": "Behin eskaera hau berretsi ondoren, ezin izango duzu aldatu. Mesedez, arretaz berrikusi berretsi aurretik.",
|
|
},
|
|
# Also support 'eu' as a variant
|
|
"eu": {
|
|
"Draft Already Exists": "Zirriborro Dagoeneko Badago",
|
|
"A saved draft already exists for this week.": "Gordetako zirriborro bat dagoeneko badago asteburu honetarako.",
|
|
"You have two options:": "Bi aukera dituzu:",
|
|
"Option 1: Merge with Existing Draft": "1. Aukera: Existentea Duen Zirriborroarekin Batu",
|
|
"Combine your current cart with the existing draft.": "Batu zure gaur-oraingo saskia existentea duen zirriborroarekin.",
|
|
"Existing draft has": "Existentea duen zirriborroak du",
|
|
"Current cart has": "Zure gaur-oraingo saskiak du",
|
|
"item(s)": "artikulu(a)",
|
|
"Products will be merged by adding quantities. If a product exists in both, quantities will be combined.": "Produktuak batuko dira kantitateak gehituz. Produktu bat bian badago, kantitateak konbinatuko dira.",
|
|
"Option 2: Replace with Current Cart": "2. Aukera: Gaur-oraingo Askiarekin Ordeztu",
|
|
"Delete the old draft and save only the current cart items.": "Ezabatu zahar-zirriborroa eta gorde soilik gaur-oraingo saskiaren artikulua.",
|
|
"The existing draft will be permanently deleted.": "Existentea duen zirriborroa behin betiko ezabatuko da.",
|
|
"Merge": "Batu",
|
|
"Replace": "Ordeztu",
|
|
"Cancel": "Ezeztatu",
|
|
# Checkout page labels
|
|
"Home Delivery": "Etxera Bidalketa",
|
|
"Delivery Information": "Bidalketaren Informazioa",
|
|
"Your order will be delivered the day after pickup between 11:00 - 14:00": "Zure eskaera bidaliko da biltzeko eguaren ondoren 11:00 - 14:00 bitartean",
|
|
"Important": "Garrantzitsua",
|
|
"Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.": "Behin eskaera hau berretsi ondoren, ezin izango duzu aldatu. Mesedez, arretaz berrikusi berretsi aurretik.",
|
|
},
|
|
}
|
|
|
|
# Get the translation dictionary for the user's language
|
|
# Try exact match first, then try without the region code (e.g., 'eu' from 'eu_ES')
|
|
lang_translations = translations.get(lang)
|
|
if not lang_translations and "_" in lang:
|
|
lang_code = lang.split("_")[0] # Get 'eu' from 'eu_ES'
|
|
lang_translations = translations.get(lang_code, {})
|
|
if not lang_translations:
|
|
lang_translations = {}
|
|
|
|
# Translate all English labels to the target language
|
|
translated = {}
|
|
for key, english_label in labels_dict.items():
|
|
translated[key] = lang_translations.get(english_label, english_label)
|
|
|
|
_logger.info(
|
|
"[_translate_labels] Language: %s, Translated %d labels",
|
|
lang,
|
|
len(translated),
|
|
)
|
|
|
|
return translated
|
|
|
|
@http.route(
|
|
["/eskaera/labels", "/eskaera/i18n"],
|
|
type="json",
|
|
auth="public",
|
|
website=True,
|
|
csrf=False,
|
|
)
|
|
def get_checkout_labels(self, **post):
|
|
"""Return ALL translated UI labels and messages unified.
|
|
|
|
This is the SINGLE API ENDPOINT for fetching all user-facing translations.
|
|
Use this from JavaScript instead of maintaining local translation files.
|
|
|
|
The endpoint automatically detects the user's language and returns all
|
|
UI labels/messages in that language, ready to be used directly.
|
|
|
|
Returns:
|
|
dict: Complete set of translated labels and messages
|
|
"""
|
|
try:
|
|
lang = self._get_detected_language(**post)
|
|
labels = self._get_translated_labels(lang)
|
|
|
|
_logger.info(
|
|
"[get_checkout_labels] ✅ SUCCESS - Language: %s, Label count: %d",
|
|
lang,
|
|
len(labels),
|
|
)
|
|
|
|
return labels
|
|
except Exception as e:
|
|
_logger.error("[get_checkout_labels] ❌ ERROR: %s", str(e), exc_info=True)
|
|
# Return default English labels as fallback
|
|
return {
|
|
"save_cart": "Save Cart",
|
|
"reload_cart": "Reload Cart",
|
|
"empty_cart": "Your cart is empty",
|
|
"added_to_cart": "added to cart",
|
|
}
|