[FIX] website_sale_aplicoop: Complete infinite scroll and search filter integration

Major fixes:
- Fix JSON body parsing in load_products_ajax with type='http' route
  * Parse JSON from request.httprequest.get_data() instead of post params
  * Correctly read page, search, category from JSON request body

- Fix search and category filter combination
  * Use intersection (&) instead of replacement to preserve both filters
  * Now respects search AND category simultaneously

- Integrate realtime_search.js with infinite_scroll.js
  * Add resetWithFilters() method to reset scroll to page 1 with new filters
  * When search/category changes, reload products from server
  * Clear grid and load fresh results

- Fix pagination reset logic
  * Set currentPage = 0 in resetWithFilters() so loadNextPage() increments to 1
  * Prevents loading empty page 2 when resetting filters

Results:
✅ Infinite scroll loads all pages correctly (1, 2, 3...)
✅ Search filters work across all products (not just loaded)
✅ Category filters work correctly
✅ Search AND category filters work together
✅ Page resets to 1 when filters change
This commit is contained in:
snt 2026-02-17 00:28:17 +01:00
parent 534876242e
commit 5eb039ffe0
4 changed files with 603 additions and 71 deletions

View file

@ -847,16 +847,18 @@ class AplicoopWebsiteSale(WebsiteSale):
# - 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)
all_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),
len(all_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)
product_categories = all_products.mapped("categ_id").filtered(
lambda c: c.id > 0
)
# Collect all categories including parent chain
all_categories_set = set()
@ -884,19 +886,24 @@ class AplicoopWebsiteSale(WebsiteSale):
search_query = post.get("search", "").strip()
category_filter = post.get("category", "0")
# Apply search
# ===== IMPORTANT: Filter COMPLETE catalog BEFORE pagination =====
# This ensures search works on full catalog and tags show correct counts
filtered_products = all_products
# Apply search to COMPLETE catalog
if search_query:
products = products.filtered(
filtered_products = filtered_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',
'eskaera_shop: Filtered by search "%s". Found %d of %d total',
search_query,
len(products),
len(filtered_products),
len(all_products),
)
# Apply category filter
# Apply category filter to COMPLETE catalog
if category_filter != "0":
try:
category_id = int(category_filter)
@ -916,7 +923,7 @@ class AplicoopWebsiteSale(WebsiteSale):
# 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(
cat_filtered = request.env["product.product"].search(
[
("categ_id", "in", all_category_ids),
("active", "=", True),
@ -939,32 +946,65 @@ class AplicoopWebsiteSale(WebsiteSale):
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(
cat_filtered = cat_filtered.filtered(
lambda p: p.categ_id.id in order_cat_ids
)
products = filtered_products
filtered_products = cat_filtered
_logger.info(
"eskaera_shop: Filtered by category %d and descendants. Found %d products",
"eskaera_shop: Filtered by category %d and descendants. Found %d of %d total",
category_id,
len(products),
len(filtered_products),
len(all_products),
)
except (ValueError, TypeError) as e:
_logger.warning("eskaera_shop: Invalid category filter: %s", str(e))
# Apply pagination if lazy loading enabled
total_products = len(products)
# ===== Calculate available tags BEFORE pagination (on complete filtered set) =====
available_tags_dict = {}
for product in filtered_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
available_tags_dict[tag.id] = {
"id": tag.id,
"name": tag.name,
"color": tag_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 filtered products",
len(available_tags),
len(filtered_products),
)
# ===== NOW apply pagination to the FILTERED results =====
total_products = len(filtered_products)
has_next = False
products = filtered_products
if lazy_loading_enabled:
offset = (page - 1) * per_page
products = products[offset : offset + 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",
"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,
)
# Prepare supplier info dict: {product.id: 'Supplier (City)'}
@ -1072,44 +1112,6 @@ class AplicoopWebsiteSale(WebsiteSale):
)
product_display_info[product.id] = display_info
# Calculate available tags with product count (only show tags that are actually used and visible)
# Build a dict: {tag_id: {'id': tag_id, 'name': tag_name, 'count': num_products}}
available_tags_dict = {}
for product in products:
for tag in product.product_tag_ids:
# Only include tags that are visible on ecommerce
is_visible = getattr(
tag, "visible_on_ecommerce", True
) # Default to True if field doesn't exist
if not is_visible:
continue
if tag.id not in available_tags_dict:
tag_color = tag.color if tag.color else None
_logger.info(
"Tag %s (id=%s): color=%s (type=%s)",
tag.name,
tag.id,
tag_color,
type(tag_color),
)
available_tags_dict[tag.id] = {
"id": tag.id,
"name": tag.name,
"color": tag_color, # Use tag color (hex) or None for theme color
"count": 0,
}
available_tags_dict[tag.id]["count"] += 1
# Convert to sorted list of tags (sorted by name for consistent display)
available_tags = sorted(available_tags_dict.values(), key=lambda t: t["name"])
_logger.info(
"eskaera_shop: Found %d available tags for %d products",
len(available_tags),
len(products),
)
_logger.info("eskaera_shop: available_tags = %s", available_tags)
# Manage session for separate cart per order
session_key = f"eskaera_{order_id}"
cart = request.session.get(session_key, {})
@ -1119,10 +1121,10 @@ class AplicoopWebsiteSale(WebsiteSale):
# Filter product tags to only show published ones
# Create a dictionary with filtered tags for each product
filtered_products = {}
filtered_products_dict = {}
for product in products:
published_tags = self._filter_published_tags(product.product_tag_ids)
filtered_products[product.id] = {
filtered_products_dict[product.id] = {
"product": product,
"published_tags": published_tags,
}
@ -1132,7 +1134,7 @@ class AplicoopWebsiteSale(WebsiteSale):
{
"group_order": group_order,
"products": products,
"filtered_product_tags": filtered_products,
"filtered_product_tags": filtered_products_dict,
"cart": cart,
"available_categories": available_categories,
"category_hierarchy": category_hierarchy,
@ -1163,9 +1165,10 @@ class AplicoopWebsiteSale(WebsiteSale):
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"].browse(order_id)
group_order = request.env["group_order"].browse(order_id)
if not group_order.exists() or group_order.state != "open":
return ""
@ -1193,16 +1196,95 @@ class AplicoopWebsiteSale(WebsiteSale):
)
# Get all products (same logic as eskaera_shop)
products = group_order._get_products_for_group_order(group_order.id)
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")
# ===== Apply SAME filters as eskaera_shop =====
filtered_products = all_products
# Apply search
if search_query:
filtered_products = filtered_products.filtered(
lambda p: search_query.lower() in p.name.lower()
or search_query.lower() in (p.description or "").lower()
)
_logger.info(
'load_eskaera_page: search filter "%s" - found %d of %d',
search_query,
len(filtered_products),
len(all_products),
)
# Apply category filter
if category_filter != "0":
try:
category_id = int(category_filter)
selected_category = request.env["product.category"].browse(category_id)
if selected_category.exists():
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)
cat_filtered = 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),
]
)
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)
cat_filtered = cat_filtered.filtered(
lambda p: p.categ_id.id in order_cat_ids
)
filtered_products = cat_filtered
_logger.info(
"load_eskaera_page: category filter %d - found %d of %d",
category_id,
len(filtered_products),
len(all_products),
)
except (ValueError, TypeError):
_logger.warning("load_eskaera_page: Invalid category filter")
# ===== Apply pagination to FILTERED results =====
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
pricelist = self._resolve_pricelist()
# Calculate prices only for products on this page
offset = (page - 1) * per_page
products_page = products[offset : offset + per_page]
has_next = offset + per_page < len(products)
# Calculate prices for this page
product_price_info = {}
for product in products_page:
product_variant = (
@ -1261,10 +1343,10 @@ class AplicoopWebsiteSale(WebsiteSale):
product_supplier_info[product.id] = supplier_name
# Filter product tags
filtered_products = {}
filtered_products_dict = {}
for product in products_page:
published_tags = self._filter_published_tags(product.product_tag_ids)
filtered_products[product.id] = {
filtered_products_dict[product.id] = {
"product": product,
"published_tags": published_tags,
}
@ -1286,7 +1368,7 @@ class AplicoopWebsiteSale(WebsiteSale):
{
"group_order": group_order,
"products": products_page,
"filtered_product_tags": filtered_products,
"filtered_product_tags": filtered_products_dict,
"product_supplier_info": product_supplier_info,
"product_price_info": product_price_info,
"product_display_info": product_display_info,
@ -1296,6 +1378,207 @@ class AplicoopWebsiteSale(WebsiteSale):
},
)
@http.route(
["/eskaera/<int:order_id>/load-products-ajax"],
type="json",
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"].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"].get_param(
"website_sale_aplicoop.products_per_page", 20
)
)
# Get page from POST
try:
page = int(post.get("page", 1))
if page < 1:
page = 1
except (ValueError, TypeError):
page = 1
# Get filters
search_query = post.get("search", "").strip()
category_filter = str(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
all_products = group_order._get_products_for_group_order(group_order.id)
filtered_products = all_products
# Apply search
if search_query:
filtered_products = filtered_products.filtered(
lambda p: search_query.lower() in p.name.lower()
or search_query.lower() in (p.description or "").lower()
)
# Apply category filter
if category_filter != "0":
try:
category_id = int(category_filter)
selected_category = request.env["product.category"].browse(category_id)
if selected_category.exists():
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)
cat_filtered = 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),
]
)
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)
cat_filtered = cat_filtered.filtered(
lambda p: p.categ_id.id in order_cat_ids
)
filtered_products = cat_filtered
except (ValueError, TypeError) as e:
_logger.warning(
"load_products_ajax: Invalid category filter: %s", str(e)
)
# 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
# Get prices
pricelist = self._resolve_pricelist()
product_price_info = {}
for product in products_page:
product_variant = (
product.product_variant_ids[0] if product.product_variant_ids else False
)
if product_variant and pricelist:
try:
price_info = product_variant._get_price(
qty=1.0,
pricelist=pricelist,
fposition=request.website.fiscal_position_id,
)
product_price_info[product.id] = {
"price": price_info.get("value", 0.0),
"list_price": price_info.get("original_value", 0.0),
"has_discounted_price": price_info.get("discount", 0.0) > 0,
"discount": price_info.get("discount", 0.0),
"tax_included": price_info.get("tax_included", True),
}
except Exception:
product_price_info[product.id] = {
"price": product.list_price,
"list_price": product.list_price,
"has_discounted_price": False,
"discount": 0.0,
"tax_included": False,
}
else:
product_price_info[product.id] = {
"price": product.list_price,
"list_price": product.list_price,
"has_discounted_price": False,
"discount": 0.0,
"tax_included": False,
}
# Prepare display info
product_display_info = {}
for product in products_page:
display_info = self._prepare_product_display_info(
product, product_price_info
)
product_display_info[product.id] = display_info
# Prepare supplier info
product_supplier_info = {}
for product in products_page:
supplier_name = ""
if product.seller_ids:
partner = product.seller_ids[0].partner_id.sudo()
supplier_name = partner.name or ""
if partner.city:
supplier_name += f" ({partner.city})"
product_supplier_info[product.id] = supplier_name
# Filter tags
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,
}
# Render HTML
html = request.env["ir.ui.view"]._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": self.get_checkout_labels(),
"has_next": has_next,
"next_page": page + 1,
},
)
return {
"html": html,
"has_next": has_next,
"next_page": page + 1,
"total": total_products,
"page": page,
}
@http.route(
["/eskaera/add-to-cart"],
type="http",