2459 lines
95 KiB
Python
2459 lines
95 KiB
Python
# 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/<int:order_id>"], type="http", auth="user", website=True)
|
|
def eskaera_shop(self, order_id, **post):
|
|
"""Página de tienda para un pedido específico (eskaera).
|
|
|
|
Muestra productos del pedido y gestiona el carrito separado.
|
|
Soporta búsqueda y filtrado por categoría.
|
|
"""
|
|
group_order = request.env["group.order"].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/<int:order_id>/load-page"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
methods=["GET"],
|
|
)
|
|
def load_eskaera_page(self, order_id, **post):
|
|
"""Load next page of products for lazy loading.
|
|
|
|
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/<int:order_id>/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/<int:order_id>/checkout"], type="http", auth="user", website=True
|
|
)
|
|
def eskaera_checkout(self, order_id, **post):
|
|
"""Checkout page to close the cart for the order (eskaera)."""
|
|
group_order = request.env["group.order"].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/<int:group_order_id>/load-from-history/<int:sale_order_id>"],
|
|
type="http",
|
|
auth="user",
|
|
website=True,
|
|
)
|
|
def load_order_from_history(self, group_order_id=None, sale_order_id=None, **post):
|
|
"""Load a historical order (draft/confirmed) back into the cart.
|
|
|
|
Used by portal "Load in Cart" button on My Orders page.
|
|
Extracts items from the order and redirects to the group order page,
|
|
where the JavaScript auto-load will populate the cart.
|
|
"""
|
|
try:
|
|
# Get the sale.order record
|
|
sale_order = request.env["sale.order"].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/<int:group_order_id>/confirm/<int:sale_order_id>"],
|
|
type="json",
|
|
auth="user",
|
|
website=True,
|
|
methods=["POST"],
|
|
)
|
|
def confirm_order_from_portal(
|
|
self, group_order_id=None, sale_order_id=None, **post
|
|
):
|
|
"""Confirm a draft order from the portal (AJAX endpoint).
|
|
|
|
Used by portal "Confirm" button on My Orders page.
|
|
Confirms the draft order and returns JSON response.
|
|
Does NOT redirect - the calling JavaScript handles the response.
|
|
"""
|
|
_logger.info(
|
|
"confirm_order_from_portal called: group_order_id=%s, sale_order_id=%s",
|
|
group_order_id,
|
|
sale_order_id,
|
|
)
|
|
|
|
try:
|
|
# Get the sale.order record
|
|
sale_order = request.env["sale.order"].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")
|