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