[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",