# 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 _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, ) @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/"], 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")) # 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)) # 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 = 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( "eskaera_shop: Using configured Aplicoop pricelist: %s (id=%s, currency=%s)", pricelist.name, pricelist.id, pricelist.currency_id.name if pricelist.currency_id else "None", ) else: pricelist = None _logger.warning( "eskaera_shop: Configured Aplicoop pricelist (id=%s) not found", aplicoop_pricelist_id, ) except Exception as e: _logger.warning( "eskaera_shop: Error getting configured Aplicoop pricelist: %s", str(e), ) # Fallback to website pricelist if not pricelist: try: pricelist = request.website._get_current_pricelist() _logger.info( "eskaera_shop: Using website pricelist: %s (id=%s, currency=%s)", pricelist.name if pricelist else "None", pricelist.id if pricelist else "None", ( pricelist.currency_id.name if pricelist and pricelist.currency_id else "None" ), ) except Exception as e: _logger.warning( "eskaera_shop: Error getting pricelist from website: %s. Trying default pricelist.", str(e), ) # Final fallback to any active pricelist if not pricelist: pricelist = request.env["product.pricelist"].search( [("active", "=", True)], limit=1 ) if pricelist: _logger.info( "eskaera_shop: Using first active pricelist as fallback: %s (id=%s)", pricelist.name, pricelist.id, ) if not pricelist: _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, } # 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, "labels": labels, "labels_json": json.dumps(labels, ensure_ascii=False), }, ) @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 # 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( "add_to_eskaera_cart: Using configured Aplicoop pricelist: %s (id=%s)", pricelist.name, pricelist.id, ) else: pricelist = None except Exception as e: _logger.warning( "add_to_eskaera_cart: Error getting configured Aplicoop pricelist: %s", str(e), ) # Fallback to website pricelist if not pricelist: try: pricelist = request.website._get_current_pricelist() _logger.info( "add_to_eskaera_cart: Using website pricelist: %s (id=%s)", pricelist.name if pricelist else "None", pricelist.id if pricelist else "None", ) except Exception as e: _logger.warning( "add_to_eskaera_cart: Error getting website pricelist: %s", str(e), ) # Final fallback to any active pricelist if not pricelist: pricelist = request.env["product.pricelist"].search( [("active", "=", True)], limit=1 ) if pricelist: _logger.info( "add_to_eskaera_cart: Using first active pricelist: %s (id=%s)", pricelist.name, pricelist.id, ) 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//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 order_id order_id = data.get("order_id") if not order_id: _logger.warning("confirm_eskaera: 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("confirm_eskaera: 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, ) _logger.info("order_id: %d", order_id) # Verify that the order exists group_order = request.env["group.order"].browse(order_id) if not group_order.exists(): _logger.warning("confirm_eskaera: 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, ) # Verify that the order is open if group_order.state != "open": _logger.warning( "confirm_eskaera: Order %d is not open (state: %s)", order_id, group_order.state, ) return request.make_response( json.dumps({"error": f"Order is {group_order.state}"}), [("Content-Type", "application/json")], status=400, ) current_user = request.env.user _logger.info("Current user: %d", current_user.id) # Validate that the user has a partner_id if not current_user.partner_id: _logger.error( "confirm_eskaera: 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 and delivery status items = data.get("items", []) is_delivery = data.get("is_delivery", False) if not items: _logger.warning( "confirm_eskaera: 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, ) # 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 # 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( "confirm_eskaera: 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("Adding sale order line: %s", line_data) sale_order_lines.append((0, 0, line_data)) except (ValueError, TypeError) as e: _logger.warning( "confirm_eskaera: Error processing item %s: %s", item, str(e) ) continue if not sale_order_lines: _logger.warning("confirm_eskaera: No valid items for sale.order") return request.make_response( json.dumps({"error": "No valid items in cart"}), [("Content-Type", "application/json")], status=400, ) # 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 a localized confirmation message on the server so the # client only needs to display the final string. Use `_()` to # mark strings for translation and `_get_day_names()` to obtain # the translated day name according to the user's language. try: pickup_day_index = int(group_order.pickup_day) except Exception: pickup_day_index = None 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}" ) 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}" 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//load-from-history/"], 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//confirm/"], 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", }