# Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) import json import logging from odoo import fields from odoo import http from odoo.http import request from odoo.addons.website_sale.controllers.main import WebsiteSale from . import website_sale_i18n as _i18n from . import website_sale_pickup as _pickup from . import website_sale_pricing as _pricing from . import website_sale_products as _products from . import website_sale_utils as _utils from . import website_sale_validators as _validators from .exceptions import BadRequestError from .exceptions import ForbiddenError from .exceptions import GroupOrderUnavailable _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): """Delegate day names lookup to pickup helper module.""" return _pickup._get_day_names(self, env=env, request_obj=request) def _get_next_date_for_weekday(self, weekday_num, start_date=None): """Delegate next-date computation to pickup helper.""" return _pickup._get_next_date_for_weekday( self, weekday_num, start_date=start_date ) def _get_detected_language(self, **post): return _i18n._get_detected_language(self, request, **post) def _get_translated_labels(self, lang=None): return _i18n._get_translated_labels(self, lang, request) def _build_category_hierarchy(self, categories): return _products._build_category_hierarchy(self, categories) # ========== PHASE 1: HELPER METHODS FOR VALIDATION AND CONFIGURATION ========== def _resolve_pricelist(self): return _pricing._resolve_pricelist(self, request) def _prepare_product_display_info(self, product, product_price_info): return _pricing._prepare_product_display_info( self, product, product_price_info, request, ) def _get_pricing_info(self, product, pricelist, quantity=1.0, partner=None): return _pricing._get_pricing_info( self, product, pricelist, quantity=quantity, partner=partner, request_obj=request, ) def _compute_price_info(self, products, pricelist): return _pricing._compute_price_info(self, products, pricelist, request) def _get_product_supplier_info(self, products): return _pricing._get_product_supplier_info(self, products) def _get_delivery_product_display_price(self, delivery_product, pricelist=None): return _pricing._get_delivery_product_display_price( self, delivery_product, pricelist=pricelist, request_obj=request, ) def _filter_products(self, all_products, post, group_order): return _products._filter_products(self, all_products, post, group_order) def _validate_confirm_request(self, data): return _validators._validate_confirm_request(self, data, request) 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 """ return _validators._validate_draft_request(self, data, request) 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 """ return _validators._validate_confirm_json(self, data, request) def _to_bool(self, value): return _validators._to_bool(self, value) def _get_effective_delivery_context(self, group_order, is_delivery): """Return effective home delivery flag and commitment date. Delivery is effective only when requested by the user and enabled in the group order configuration. """ delivery_requested = self._to_bool(is_delivery) delivery_enabled = bool(group_order and group_order.home_delivery) effective_home_delivery = delivery_requested and delivery_enabled if delivery_requested and not delivery_enabled: _logger.info( "Delivery requested but disabled in group order %s; using pickup flow", group_order.id if group_order else "N/A", ) if effective_home_delivery: commitment_date = group_order.delivery_date or group_order.pickup_date else: commitment_date = group_order.pickup_date if group_order else False return effective_home_delivery, commitment_date def _process_cart_items(self, items, group_order, pricelist=None): """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 = [] pricelist = pricelist or self._resolve_pricelist() partner = request.env.user.partner_id for item in items: try: product_id = int(item.get("product_id")) quantity = float(item.get("quantity", 1)) product = request.env["product.product"].sudo().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 pricing = self._get_pricing_info( product, pricelist, quantity=quantity, partner=partner, ) line_data = { "product_id": product_id, "product_uom_qty": quantity, "price_unit": pricing.get("price_unit", 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 _get_salesperson_for_order(self, partner): return _validators._get_salesperson_for_order(self, partner) def _get_consumer_group_for_user(self, group_order, current_user): return _validators._get_consumer_group_for_user(self, group_order, current_user) def _validate_user_group_access(self, group_order, current_user): return _validators._validate_user_group_access(self, group_order, current_user) def _create_or_update_sale_order( self, group_order, current_user, sale_order_lines, is_delivery, commitment_date=None, existing_order=None, ): """Create or update a sale.order from prepared sale_order_lines. Returns the sale.order record. """ consumer_group_id = self._validate_user_group_access(group_order, current_user) _logger.info( "[CONSUMER_GROUP DEBUG] _create_or_update_sale_order: " "group_order=%s, group_order.group_ids=%s, consumer_group_id=%s", group_order.id, group_order.group_ids.ids, consumer_group_id, ) effective_home_delivery, calculated_commitment_date = ( self._get_effective_delivery_context(group_order, is_delivery) ) # Explicit commitment_date wins; otherwise use calculated value if not commitment_date: commitment_date = calculated_commitment_date if existing_order: # Update existing order with new lines and propagate fields # Use sudo() to avoid permission issues with portal users existing_order_sudo = existing_order.sudo() # (5,) clears all existing lines before adding the new ones existing_order_sudo.order_line = [(5,)] + sale_order_lines if not existing_order_sudo.group_order_id: existing_order_sudo.group_order_id = group_order.id existing_order_sudo.pickup_day = group_order.pickup_day existing_order_sudo.pickup_date = group_order.pickup_date existing_order_sudo.home_delivery = effective_home_delivery existing_order_sudo.consumer_group_id = consumer_group_id if commitment_date: existing_order_sudo.commitment_date = commitment_date _logger.info( "Updated existing sale.order %d: commitment_date=%s, home_delivery=%s, consumer_group_id=%s", existing_order.id, commitment_date, effective_home_delivery, consumer_group_id, ) return existing_order # Create new order values dict 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": effective_home_delivery, "consumer_group_id": consumer_group_id, } if commitment_date: order_vals["commitment_date"] = commitment_date # Get salesperson for order creation (portal users need this) salesperson = self._get_salesperson_for_order(current_user.partner_id) if salesperson: order_vals["user_id"] = salesperson.id _logger.info( "Creating sale.order with salesperson %s (%d)", salesperson.name, salesperson.id, ) # Create order with sudo to avoid permission issues with portal users sale_order = request.env["sale.order"].sudo().create(order_vals) _logger.info( "sale.order created successfully: %d with group_order_id=%d, pickup_day=%s, home_delivery=%s, consumer_group_id=%s", sale_order.id, group_order.id, group_order.pickup_day, effective_home_delivery, consumer_group_id, ) return sale_order def _create_draft_sale_order( self, group_order, current_user, sale_order_lines, order_id, pickup_date=None, is_delivery=False, ): """Create a draft sale.order from prepared lines and propagate group fields. Returns created sale.order record. """ consumer_group_id = self._validate_user_group_access(group_order, current_user) _logger.info( "[CONSUMER_GROUP DEBUG] _create_draft_sale_order: " "group_order=%s, group_order.group_ids=%s, consumer_group_id=%s", group_order.id, group_order.group_ids.ids, consumer_group_id, ) effective_home_delivery, commitment_date = self._get_effective_delivery_context( group_order, is_delivery ) order_vals = { "partner_id": current_user.partner_id.id, "order_line": sale_order_lines, "state": "draft", "group_order_id": order_id, "pickup_day": group_order.pickup_day, "pickup_date": group_order.pickup_date, "home_delivery": effective_home_delivery, "consumer_group_id": consumer_group_id, "commitment_date": commitment_date, } # Get salesperson for order creation (portal users need this) salesperson = self._get_salesperson_for_order(current_user.partner_id) if salesperson: order_vals["user_id"] = salesperson.id _logger.info( "Creating draft sale.order with salesperson %s (%d)", salesperson.name, salesperson.id, ) # Create order with sudo to avoid permission issues with portal users sale_order = request.env["sale.order"].sudo().create(order_vals) # Ensure the order has a name (sequence) try: if not sale_order.name or sale_order.name == "New": sale_order._onchange_partner_id() if not sale_order.name or sale_order.name == "New": sale_order.name = "DRAFT-%s" % sale_order.id except Exception as exc: # Do not break creation on name generation issues _logger.warning( "Failed to generate name for draft sale order %s: %s", sale_order.id, exc, ) return sale_order def _build_confirmation_message(self, sale_order, group_order, is_delivery): """Build localized confirmation message for confirm_eskaera.""" # Get pickup day index, localized name and date string using helper pickup_day_name, pickup_date_str, pickup_day_index = self._format_pickup_info( group_order, is_delivery ) # Initialize translatable strings base_message = request.env._("Thank you! Your order has been confirmed.") order_reference_label = request.env._("Order reference") pickup_label = request.env._("Pickup day") delivery_label = request.env._("Delivery date") # Add order reference to message if sale_order.name: base_message = ( f"{base_message}\n\n{order_reference_label}: {sale_order.name}" ) # 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, } def _format_pickup_info(self, group_order, is_delivery): return _pickup._format_pickup_info( self, group_order, is_delivery, request_obj=request, ) def _slot_time_label(self, slot): return _pickup._slot_time_label(self, slot) def _format_datetime_to_str(self, dt_val): return _pickup._format_datetime_to_str(self, dt_val) def _format_slot_pickup_info(self, group_order, slot): return _pickup._format_slot_pickup_info( self, group_order, slot, request_obj=request, ) def _format_legacy_pickup_info(self, group_order, is_delivery): return _pickup._format_legacy_pickup_info( self, group_order, is_delivery, request_obj=request, ) def _parse_save_cart_request(self): """Decode and validate the incoming save-cart request. Returns a tuple: (data, order_id, group_order, current_user, items, pickup_date, is_delivery) Raises: BadRequestError, ForbiddenError, GroupOrderUnavailable """ data = self._decode_json_body() order_id = data.get("order_id") if not order_id: raise BadRequestError("order_id is required") try: order_id = int(order_id) except (TypeError, ValueError) as e: # Preserve the original exception context raise BadRequestError(f"Invalid order_id format: {order_id}") from e group_order = request.env["group.order"].sudo().browse(order_id) if not group_order.exists(): raise BadRequestError(f"Order {order_id} not found") if group_order.state != "open": # Upstream handler expects a special response for unavailable orders raise GroupOrderUnavailable() current_user = request.env.user if not current_user.partner_id: raise BadRequestError("User has no associated partner") try: self._validate_user_group_access(group_order, current_user) except ValueError as e: # Preserve exception chaining for better debugging raise ForbiddenError(str(e)) from e items = data.get("items", []) pickup_date = data.get("pickup_date") is_delivery = self._to_bool(data.get("is_delivery", False)) if not items: raise BadRequestError("No items in cart") return ( data, order_id, group_order, current_user, items, pickup_date, is_delivery, ) @http.route(["/eskaera"], type="http", auth="user", website=True) def eskaera_list(self, **post): """Página de pedidos de grupo abiertos esta semana. Para usuarios portal: solo muestra pedidos de sus grupos de consumo asignados. Para usuarios internos: muestra todos los pedidos de la compañía. """ group_order_obj = request.env["group.order"].sudo() 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 (filtrados por company_id) active_orders = group_order_obj.get_active_orders_for_week() # Para usuarios portal: filtrar solo por sus grupos de consumo if current_user.share: consumer_group_ids = current_user.partner_id.group_ids.ids active_orders = active_orders.filtered( lambda o: any(g.id in consumer_group_ids for g in o.group_ids) ) _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): """Delegate tag filtering to products helper.""" return _products._filter_published_tags(self, tags) def _collect_all_products_and_categories(self, group_order): """Delegate product/category collection to products helper.""" return _products._collect_all_products_and_categories(self, group_order) def _prepare_products_maps(self, products, pricelist): """Delegate preparation of product maps to products helper.""" return _products._prepare_products_maps(self, products, pricelist) def _merge_or_replace_draft( self, group_order, current_user, sale_order_lines, existing_drafts, order_id, is_delivery=False, ): """Replace existing draft (if any) with new lines, else create it. All fields (commitment_date, consumer_group_id, etc.) come from group_order. """ consumer_group_id = self._validate_user_group_access(group_order, current_user) _logger.info( "[CONSUMER_GROUP DEBUG] _merge_or_replace_draft: " "group_order=%s, group_order.group_ids=%s, consumer_group_id=%s", group_order.id, group_order.group_ids.ids, consumer_group_id, ) effective_home_delivery, commitment_date = self._get_effective_delivery_context( group_order, is_delivery ) if existing_drafts: draft = existing_drafts[0].sudo() _logger.info( "Replacing existing draft order %s for partner %s", draft.id, current_user.partner_id.id, ) draft.write( { "order_line": [(5, 0, 0)] + sale_order_lines, "group_order_id": order_id, "pickup_day": group_order.pickup_day, "pickup_date": group_order.pickup_date, "home_delivery": effective_home_delivery, "consumer_group_id": consumer_group_id, "commitment_date": commitment_date, } ) return draft order_vals = { "partner_id": current_user.partner_id.id, "order_line": sale_order_lines, "state": "draft", "group_order_id": order_id, "pickup_day": group_order.pickup_day, "pickup_date": group_order.pickup_date, "home_delivery": effective_home_delivery, "consumer_group_id": consumer_group_id, "commitment_date": commitment_date, } # Get salesperson for order creation (portal users need this) salesperson = self._get_salesperson_for_order(current_user.partner_id) if salesperson: order_vals["user_id"] = salesperson.id sale_order = request.env["sale.order"].sudo().create(order_vals) return sale_order def _decode_json_body(self): return _utils._decode_json_body(self, request) def _build_group_order_unavailable_response(self, group_order, status=403): """Delegate building of unavailable response to utils helper.""" self._request = request return _utils._build_group_order_unavailable_response( self, group_order, status=status ) def _validate_items_for_group_order(self, items, group_order): """Delegate availability validation to validators helper.""" return _validators._validate_items_for_group_order( self, items, group_order, request, ) def _find_recent_draft_order(self, partner_id, group_order): """Find most recent draft sale.order for partner in the active order period. Priority for period matching: 1) If group_order.pickup_date is set, match that exact pickup_date (prevents reusing stale drafts from previous cycles of same group.order). 2) Fallback to current-week create_date bounds when pickup_date is not set. Returns the recordset (limit=1) or empty recordset. """ return _validators._find_recent_draft_order( self, partner_id, group_order, request, ) @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"].sudo().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: para usuarios portal, verificar que pertenecen a un grupo de consumo # que tiene acceso a este pedido current_user = request.env.user if current_user.share: consumer_group_ids = current_user.partner_id.group_ids.ids order_group_ids = group_order.group_ids.ids if not any(gid in consumer_group_ids for gid in order_group_ids): return request.redirect("/eskaera") # 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"] .sudo() .get_param("website_sale_aplicoop.lazy_loading_enabled", "True") == "True" ) per_page = int( request.env["ir.config_parameter"] .sudo() .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 all products and categories and build hierarchy using helper all_products, available_categories, category_hierarchy = ( self._collect_all_products_and_categories(group_order) ) _logger.info( "eskaera_shop order_id=%d, total products=%d (discovered)", order_id, len(all_products), ) # Apply search/category filters and compute available tags filtered_products, available_tags, search_query, category_filter = ( self._filter_products(all_products, post, group_order) ) # Pagination total_products = len(filtered_products) has_next = False products = filtered_products if lazy_loading_enabled: offset = (page - 1) * per_page products = filtered_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, showing %d of %d", page, offset, per_page, has_next, len(products), total_products, ) # Compute pricing and prepare maps _logger.info("eskaera_shop: Starting price calculation for order %d", order_id) pricelist = self._resolve_pricelist() ( product_price_info, product_supplier_info, product_display_info, filtered_products_dict, ) = self._prepare_products_maps(products, pricelist) # Manage session for separate cart per order session_key = f"eskaera_{order_id}" cart = request.session.get(session_key, {}) # Get delivery product from group_order (configured per group order) delivery_product = group_order.delivery_product_id 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 translated labels for JavaScript (same as checkout) labels = self.get_checkout_labels() return request.render( "website_sale_aplicoop.eskaera_shop", { "group_order": group_order, "products": products, "filtered_product_tags": filtered_products_dict, "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, "delivery_product_id": delivery_product_id, "delivery_product_name": delivery_product_name, "delivery_product_price": self._get_delivery_product_display_price( delivery_product, pricelist=pricelist ), "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//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. Respects same search/filter parameters as eskaera_shop. Returns only HTML of product cards without page wrapper. """ group_order = request.env["group.order"].sudo().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"] .sudo() .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 and apply standard filters using shared helper all_products = group_order._get_products_for_group_order(group_order.id) # Get search and filter parameters (passed via POST/GET) search_query = post.get("search", "").strip() category_filter = post.get("category", "0") filtered_products, available_tags, search_query, category_filter = ( self._filter_products( all_products, {"search": search_query, "category": category_filter}, group_order, ) ) # ===== Apply pagination to the FILTERED results using shared logic ===== total_products = len(filtered_products) offset = (page - 1) * per_page products_page = filtered_products[offset : offset + per_page] has_next = offset + per_page < total_products _logger.info( "load_eskaera_page: page=%d, offset=%d, showing %d of %d filtered", page, offset, len(products_page), total_products, ) # Get pricelist and compute prices using shared helper pricelist = self._resolve_pricelist() product_price_info = self._compute_price_info(products_page, pricelist) # Prepare supplier info and display maps using shared helpers product_supplier_info = self._get_product_supplier_info(products_page) filtered_products_dict = {} for product in products_page: published_tags = self._filter_published_tags(product.product_tag_ids) filtered_products_dict[product.id] = { "product": product, "published_tags": published_tags, } product_display_info = {} for product in products_page: product_display_info[product.id] = self._prepare_product_display_info( product, product_price_info ) labels = self.get_checkout_labels() return request.render( "website_sale_aplicoop.eskaera_shop_products", { "group_order": group_order, "products": products_page, "filtered_product_tags": filtered_products_dict, "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//load-products-ajax"], type="http", auth="user", website=True, methods=["POST"], csrf=False, ) def load_products_ajax(self, order_id, **post): """Load products via AJAX for infinite scroll. Returns JSON with: - html: rendered product cards HTML - has_next: whether there are more products - next_page: page number to fetch next - total: total filtered products """ group_order = request.env["group.order"].sudo().browse(order_id) if not group_order.exists() or group_order.state != "open": return {"error": "Order not found or not open", "html": ""} # Get configuration per_page = int( request.env["ir.config_parameter"] .sudo() .get_param("website_sale_aplicoop.products_per_page", 20) ) # Parse JSON body for parameters (type="http" doesn't auto-parse JSON) params = {} try: if request.httprequest.content_length: data = request.httprequest.get_data(as_text=True) if data: params = json.loads(data) except (ValueError, json.JSONDecodeError, AttributeError): params = {} # Get page from POST/JSON try: page = int(params.get("page", post.get("page", 1))) if page < 1: page = 1 except (ValueError, TypeError): page = 1 # Get filters search_query = params.get("search", post.get("search", "")).strip() category_filter = str(params.get("category", post.get("category", "0"))) _logger.info( "load_products_ajax: order_id=%d, page=%d, search=%s, category=%s", order_id, page, search_query, category_filter, ) # Get all products and apply shared filtering logic all_products = group_order._get_products_for_group_order(group_order.id) filtered_products, available_tags, _, _ = self._filter_products( all_products, {"search": search_query, "category": category_filter}, group_order, ) # Paginate total_products = len(filtered_products) offset = (page - 1) * per_page products_page = filtered_products[offset : offset + per_page] has_next = offset + per_page < total_products _logger.info( "load_products_ajax: Pagination - page=%d, offset=%d, per_page=%d, total=%d, has_next=%s", page, offset, per_page, total_products, has_next, ) # Compute prices and supplier/display info using shared helpers pricelist = self._resolve_pricelist() product_price_info = self._compute_price_info(products_page, pricelist) product_display_info = { product.id: self._prepare_product_display_info(product, product_price_info) for product in products_page } product_supplier_info = self._get_product_supplier_info(products_page) filtered_products_dict = { product.id: { "product": product, "published_tags": self._filter_published_tags(product.product_tag_ids), } for product in products_page } # Render HTML html = ( request.env["ir.ui.view"] .sudo() ._render_template( "website_sale_aplicoop.eskaera_shop_products", { "group_order": group_order, "products": products_page, "filtered_product_tags": filtered_products_dict, "product_supplier_info": product_supplier_info, "product_price_info": product_price_info, "product_display_info": product_display_info, "labels": self.get_checkout_labels(), "has_next": has_next, "next_page": page + 1, }, ) ) return request.make_response( json.dumps( { "html": html, "has_next": has_next, "next_page": page + 1, "total": total_products, "page": page, } ), [("Content-Type", "application/json")], ) @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"].sudo().browse(order_id) product = request.env["product.product"].sudo().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": request.env._("%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"].sudo().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 from group_order (configured per group order) delivery_product = group_order.delivery_product_id 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": self._get_delivery_product_display_price( delivery_product ), "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/check-status"], type="http", auth="user", website=True, methods=["POST"], csrf=False, ) def check_group_order_status(self, **post): """Return status information for a group.order. Intended for frontend proactive cart invalidation when the order changed state between visits. """ try: data = self._decode_json_body() except ValueError as e: return request.make_response( json.dumps({"error": 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 (TypeError, ValueError): 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"].sudo().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=404, ) # Determine if cutoff date for the current cycle has already passed try: today = fields.Date.today() cutoff_passed = False cutoff_date_str = None if group_order.cutoff_date: cutoff_passed = group_order.cutoff_date < today # Convert to ISO-like string for frontend (YYYY-MM-DD) cutoff_date_str = str(group_order.cutoff_date) except Exception: cutoff_passed = False cutoff_date_str = None response_data = { "success": True, "order_id": group_order.id, "state": group_order.state, "is_open": group_order.state == "open", "action": ( "clear_cart" if (group_order.state != "open" or cutoff_passed) else "none" ), "cutoff_passed": cutoff_passed, "cutoff_date": cutoff_date_str, } return request.make_response( json.dumps(response_data), [("Content-Type", "application/json")], ) @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. This controller delegates validation and heavy lifting to helpers so the top-level flow remains easy to follow and McCabe-friendly. """ try: _logger.warning("=== SAVE_CART_DRAFT CALLED ===") try: ( data, order_id, group_order, current_user, items, pickup_date, is_delivery, ) = self._parse_save_cart_request() except BadRequestError as e: return request.make_response( json.dumps({"error": str(e)}), [("Content-Type", "application/json")], status=400, ) except ForbiddenError as e: return request.make_response( json.dumps({"error": str(e)}), [("Content-Type", "application/json")], status=403, ) except GroupOrderUnavailable: order_id = None try: payload = self._decode_json_body() order_id = ( int(payload.get("order_id")) if payload.get("order_id") else None ) except Exception: order_id = None group_order = ( request.env["group.order"].sudo().browse(order_id) if order_id else False ) return self._build_group_order_unavailable_response(group_order) # Build sale.order lines and create draft using helpers try: sale_order_lines = self._process_cart_items( items, group_order, pricelist=self._resolve_pricelist() ) except ValueError as e: return request.make_response( json.dumps({"error": str(e)}), [("Content-Type", "application/json")], status=400, ) sale_order = self._create_draft_sale_order( group_order, current_user, sale_order_lines, order_id, pickup_date, is_delivery=is_delivery, ) _logger.info( "Draft sale.order created: %d (name: %s) for partner %d", sale_order.id, sale_order.name, current_user.partner_id.id, ) # Compute a readable pickup slot label for the response. Prefer the # order's stored computed label, otherwise derive from the group # order using the same helper the confirmation flow uses. pickup_slot_label = ( sale_order.pickup_slot_label if getattr(sale_order, "pickup_slot_label", False) else None ) if not pickup_slot_label: try: pickup_slot_label = self._format_pickup_info( group_order, is_delivery )[0] except Exception: pickup_slot_label = None return request.make_response( json.dumps( { "success": True, "message": request.env._("Cart saved as draft"), "sale_order_id": sale_order.id, "pickup_slot_label": pickup_slot_label, } ), [("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 current period.""" import json 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 body try: data = self._decode_json_body() except ValueError as e: _logger.error("Error decoding JSON: %s", str(e)) return request.make_response( json.dumps({"error": 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"].sudo().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, ) if group_order.state != "open": return self._build_group_order_unavailable_response(group_order) 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 in active period # The helper _find_recent_draft_order computes the period criteria itself, # so we only need to call it here. # Find the most recent matching draft order using helper draft_orders = self._find_recent_draft_order( current_user.partner_id.id, group_order ) if not draft_orders: error_msg = request.env._( "No draft orders found for the current order period" ) 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 = [] pricelist = self._resolve_pricelist() partner = current_user.partner_id for line in draft_order.order_line: pricing = self._get_pricing_info( line.product_id, pricelist, quantity=line.product_uom_qty, partner=partner, ) items.append( { "product_id": line.product_id.id, "product_name": line.product_id.name, "quantity": line.product_uom_qty, "product_price": pricing.get("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": request.env._("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 ), # Provide a human readable pickup slot label so the frontend # doesn't need to compute/lookup slots. This may be empty # but it's the preferred source for displaying pickup info. "pickup_slot_label": ( draft_order.pickup_slot_label if getattr(draft_order, "pickup_slot_label", False) 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/clear-cart"], type="http", auth="user", website=True, methods=["POST"], csrf=False, ) def eskaera_clear_cart(self, **post): """Clear the user's cart and cancel any existing draft sale.order. Receives: JSON body with 'order_id' Returns: JSON with success status and cancelled_order_id if applicable. """ try: data = self._decode_json_body() except ValueError as e: return request.make_response( json.dumps({"error": 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"].sudo().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=404, ) 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 and cancel any existing draft sale.order for this period cancelled_order_id = None try: draft_orders = self._find_recent_draft_order( current_user.partner_id.id, group_order ) if draft_orders: draft_order = draft_orders[0] cancelled_order_id = draft_order.id # Use action_cancel if available, otherwise unlink if draft_order.state == "draft": draft_order.sudo().action_cancel() _logger.info( "clear_cart: Cancelled draft sale.order %d for partner %d", draft_order.id, current_user.partner_id.id, ) else: _logger.info( "clear_cart: Draft order %d already in state '%s', skipping cancel", draft_order.id, draft_order.state, ) except Exception as e: _logger.warning( "clear_cart: Error cancelling draft order for partner %d: %s", current_user.partner_id.id, str(e), ) response_data = { "success": True, "message": request.env._("Cart cleared"), "cancelled_order_id": cancelled_order_id, } _logger.info( "clear_cart: Cart cleared for partner %d, order %d (cancelled_sale_order: %s)", current_user.partner_id.id, order_id, cancelled_order_id, ) return request.make_response( json.dumps(response_data), [("Content-Type", "application/json")], ) @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, delegate merge/replace to helper. """ try: _logger.warning("=== SAVE_ESKAERA_DRAFT CALLED ===") try: ( data, order_id, group_order, current_user, items, pickup_date, is_delivery, ) = self._parse_save_cart_request() except BadRequestError as e: return request.make_response( json.dumps({"error": str(e)}), [("Content-Type", "application/json")], status=400, ) except ForbiddenError as e: return request.make_response( json.dumps({"error": str(e)}), [("Content-Type", "application/json")], status=403, ) except GroupOrderUnavailable: order_id = None try: payload = self._decode_json_body() order_id = ( int(payload.get("order_id")) if payload.get("order_id") else None ) except Exception: order_id = None group_order = ( request.env["group.order"].sudo().browse(order_id) if order_id else False ) return self._build_group_order_unavailable_response(group_order) existing_drafts = self._find_recent_draft_order( current_user.partner_id.id, group_order ) _logger.info( "Creating draft sale.order with %d items for partner %d", len(items), current_user.partner_id.id, ) try: sale_order_lines = self._process_cart_items( items, group_order, pricelist=self._resolve_pricelist() ) except ValueError as e: return request.make_response( json.dumps({"error": str(e)}), [("Content-Type", "application/json")], status=400, ) sale_order = self._merge_or_replace_draft( group_order, current_user, sale_order_lines, existing_drafts, order_id, is_delivery=is_delivery, ) _logger.info( "Draft sale.order created/updated: %d for partner %d", sale_order.id, current_user.partner_id.id, ) pickup_slot_label = ( sale_order.pickup_slot_label if getattr(sale_order, "pickup_slot_label", False) else None ) if not pickup_slot_label: try: pickup_slot_label = self._format_pickup_info( group_order, is_delivery )[0] except Exception: pickup_slot_label = None return request.make_response( json.dumps( { "success": True, "message": request.env._("Order saved as draft"), "sale_order_id": sale_order.id, "pickup_slot_label": pickup_slot_label, } ), [("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", ) # Decode JSON and validate using helpers try: data = self._decode_json_body() 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, ) 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, pricelist=self._resolve_pricelist() ) 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, ) # Reuse only draft from the current group_order cycle (not stale ones) existing_order = self._find_recent_draft_order( current_user.partner_id.id, group_order ) if existing_order: _logger.info( "Found existing draft order: %d, updating instead of creating new", existing_order.id, ) else: _logger.info( "No existing draft order found, will create new sale.order" ) existing_order = None effective_home_delivery, commitment_date = ( self._get_effective_delivery_context(group_order, is_delivery) ) # Create or update sale.order using helper sale_order = self._create_or_update_sale_order( group_order, current_user, sale_order_lines, effective_home_delivery, commitment_date=commitment_date, existing_order=existing_order, ) # Build confirmation message using helper message_data = self._build_confirmation_message( sale_order, group_order, effective_home_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, } # Also include the human-readable pickup slot label to simplify # client-side rendering (may be None). response_data["pickup_slot_label"] = ( sale_order.pickup_slot_label if getattr(sale_order, "pickup_slot_label", False) else pickup_day_name ) # 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"].sudo().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) # Get the current group_order (the one being viewed, not necessarily the one from the history) group_order = request.env["group.order"].sudo().browse(group_order_id) if not group_order.exists(): return request.redirect("/shop") # Extract items from the order (skip delivery product) # Use the delivery_product_id from the group_order delivery_product = group_order.delivery_product_id 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 } ) # Validate items against current group order availability validation_result = self._validate_items_for_group_order(items, group_order) available_items = validation_result["available_items"] unavailable_items = validation_result["unavailable_items"] warning_message = validation_result["warning_message"] _logger.info( "load_order_from_history: Loaded %d items, %d available, %d unavailable from sale_order %d into group_order %d", len(items), len(available_items), len(unavailable_items), sale_order_id, group_order_id, ) # 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 ) # Only restore a human-readable label for the pickup slot. Do NOT # restore or expose internal slot IDs to the frontend. pickup_slot_label_to_restore = ( sale_order.pickup_slot_label if same_group_order and sale_order.pickup_slot_label 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( available_items ), # Pass ONLY available items "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) # Do NOT pass slot IDs to the client. Only the readable # label is useful for the UI and is safe to expose. "pickup_slot_label": pickup_slot_label_to_restore, "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 "unavailable_items": unavailable_items, # List of unavailable items "warning_message": warning_message, # Warning about unavailable products "has_unavailable_items": len(unavailable_items) > 0, # Boolean flag for template }, ), ) 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"].sudo().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": request.env._("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 the current order period.": "Un borrador guardado ya existe para el período actual del pedido.", "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 the current order period.": "Gordetako zirriborro bat dagoeneko badago uneko eskaera-aldirako.", "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 the current order period.": "Gordetako zirriborro bat dagoeneko badago uneko eskaera-aldirako.", "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", } # ================================================================ # CART REDIRECT METHODS - Redirect /shop/cart routes to /eskaera # ================================================================ @http.route(["/shop/cart"], type="http", auth="public", website=True) def cart_redirect(self, access_token=None, revive="", **post): """Redirect /shop/cart to /eskaera (no standard cart).""" _logger.info("🛒 Redirecting /shop/cart → /eskaera") return http.redirect_with_hash("/eskaera") @http.route( ["/shop/cart/update"], type="http", auth="public", website=True, methods=["POST"], ) def cart_update_redirect(self, **post): """Redirect /shop/cart/update to /eskaera (no standard cart).""" _logger.info("🛒 Redirecting /shop/cart/update → /eskaera") return http.redirect_with_hash("/eskaera") @http.route( ["/shop/cart/update_json"], type="http", auth="public", website=True, methods=["POST"], csrf=False, ) def cart_update_json_redirect(self, **post): """Redirect /shop/cart/update_json to /eskaera (no standard cart).""" _logger.info("🛒 Redirecting /shop/cart/update_json → /eskaera") return http.redirect_with_hash("/eskaera") @http.route( ["/shop/cart_quantity"], type="http", auth="public", website=True, methods=["GET"], ) def cart_quantity_redirect(self): """Redirect /shop/cart_quantity to /eskaera (no standard cart).""" _logger.info("🛒 Redirecting /shop/cart_quantity → /eskaera") return http.redirect_with_hash("/eskaera")