chore: Cleanup Query Engine and Product query API
- Resolved merge conflicts in item_group.py
- Separate api.py file for product listing backend api
- Brought back ORM in query engine, handled missing cases (website item groups, etc)
- Return results from API in a descriptive manner, helps keep sanity in JS
- On toggling views store view preference in localStorage
diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py
new file mode 100644
index 0000000..081e8a9
--- /dev/null
+++ b/erpnext/e_commerce/api.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.utils import cint
+
+from erpnext.e_commerce.product_query import ProductQuery
+from erpnext.e_commerce.filters import ProductFiltersBuilder
+from erpnext.setup.doctype.item_group.item_group import get_child_groups
+
+@frappe.whitelist(allow_guest=True)
+def get_product_filter_data():
+ """Get pre-rendered filtered products and discount filters on load."""
+ if frappe.form_dict:
+ search = frappe.form_dict.search
+ field_filters = frappe.parse_json(frappe.form_dict.field_filters)
+ attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
+ start = cint(frappe.parse_json(frappe.form_dict.start)) if frappe.form_dict.start else 0
+ item_group = frappe.form_dict.item_group
+ else:
+ search, attribute_filters, item_group = None, None, None
+ field_filters = {}
+ start = 0
+
+ sub_categories = []
+ if item_group:
+ field_filters['item_group'] = item_group
+ sub_categories = get_child_groups(item_group)
+
+ engine = ProductQuery()
+ result = engine.query(attribute_filters, field_filters, search_term=search,
+ start=start, item_group=item_group)
+
+ # discount filter data
+ filters = {}
+ discounts = result["discounts"]
+
+ if discounts:
+ filter_engine = ProductFiltersBuilder()
+ filters["discount_filters"] = filter_engine.get_discount_filters(discounts)
+
+ return {
+ "items": result["items"] or [],
+ "filters": filters,
+ "settings": engine.settings,
+ "sub_categories": sub_categories,
+ "items_count": result["items_count"]
+ }
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py
index 98d680b..954ab0f 100644
--- a/erpnext/e_commerce/doctype/website_item/website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/website_item.py
@@ -2,11 +2,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
import json
import itertools
-from six import string_types
from frappe import _
from frappe.website.website_generator import WebsiteGenerator
@@ -16,13 +14,12 @@
from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
-# SEARCH
+# SEARCH
from erpnext.e_commerce.website_item_indexing import (
- insert_item_to_index,
- update_index_for_item,
+ insert_item_to_index,
+ update_index_for_item,
delete_item_from_index
)
-# -----
class WebsiteItem(WebsiteGenerator):
website = frappe._dict(
@@ -397,7 +394,7 @@
if not doc:
return
- if isinstance(doc, string_types):
+ if isinstance(doc, str):
doc = json.loads(doc)
if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}):
@@ -419,7 +416,7 @@
# Add to search cache
insert_item_to_index(website_item)
-
+
return [website_item.name, website_item.web_item_name]
def on_doctype_update():
@@ -442,38 +439,4 @@
customer = link.link_name
break
- return True if customer else False
-
-@frappe.whitelist(allow_guest=True)
-def get_product_filter_data():
- """Get pre-rendered filtered products and discount filters on load."""
- from erpnext.e_commerce.product_query import ProductQuery
- from erpnext.e_commerce.filters import ProductFiltersBuilder
- from erpnext.setup.doctype.item_group.item_group import get_child_groups
-
- if frappe.form_dict:
- search = frappe.form_dict.search
- field_filters = frappe.parse_json(frappe.form_dict.field_filters)
- attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
- start = cint(frappe.parse_json(frappe.form_dict.start)) if frappe.form_dict.start else 0
- item_group = frappe.form_dict.item_group
- else:
- search, attribute_filters, item_group = None, None, None
- field_filters = {}
- start = 0
-
- sub_categories = []
- if item_group:
- field_filters['item_group'] = item_group
- sub_categories = get_child_groups(item_group)
-
- engine = ProductQuery()
- items, discounts = engine.query(attribute_filters, field_filters, search_term=search, start=start)
-
- # discount filter data
- filters = {}
- if discounts:
- filter_engine = ProductFiltersBuilder()
- filters["discount_filters"] = filter_engine.get_discount_filters(discounts)
-
- return items or [], filters, engine.settings, sub_categories
\ No newline at end of file
+ return True if customer else False
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_configurator/test_product_configurator.py b/erpnext/e_commerce/product_configurator/test_product_configurator.py
index abc1f30..cc6aa99 100644
--- a/erpnext/e_commerce/product_configurator/test_product_configurator.py
+++ b/erpnext/e_commerce/product_configurator/test_product_configurator.py
@@ -95,13 +95,16 @@
# check if item is visible in its own Item Group's page
engine = ProductQuery()
- items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
+ result = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
+ items = result["items"]
+
self.assertEqual(len(items), 1)
self.assertEqual(items[0].item_code, "Portal Item")
# check if item is visible in configured foreign Item Group's page
engine = ProductQuery()
- items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
+ result = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
+ items = result["items"]
item_codes = [row.item_code for row in items]
def publish_items_on_website(self):
diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py
index ed3c18b..b368252 100644
--- a/erpnext/e_commerce/product_query.py
+++ b/erpnext/e_commerce/product_query.py
@@ -18,16 +18,14 @@
page_length (Int): Length of page for the query
settings (Document): E Commerce Settings DocType
"""
-
def __init__(self):
self.settings = frappe.get_doc("E Commerce Settings")
self.page_length = self.settings.products_per_page or 20
- self.fields = ['wi.web_item_name', 'wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of',
- 'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description',
- 'wi.route', 'wi.website_warehouse']
- self.conditions = ""
- self.or_conditions = ""
- self.substitutions = []
+ self.fields = ['web_item_name', 'name', 'item_name', 'item_code', 'website_image',
+ 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description',
+ 'description', 'route', 'website_warehouse', 'ranking']
+ self.filters = [["published", "=", 1]]
+ self.or_filters = []
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
"""Summary
@@ -42,19 +40,28 @@
list: List of results with set fields
"""
result, discount_list = [], []
+ website_item_groups = []
+
+ # if from item group page consider website item group table
+ if item_group:
+ website_item_groups = frappe.db.get_all(
+ "Website Item",
+ fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
+ filters=[["Website Item Group", "item_group", "=", item_group]]
+ )
if fields:
self.build_fields_filters(fields)
if search_term:
self.build_search_filters(search_term)
if self.settings.hide_variants:
- self.conditions += " and wi.variant_of is null"
+ self.filters.append(["variant_of", "is", "not set"])
+ count = 0
if attributes:
- result = self.query_items_with_attributes(attributes, start)
+ result, count = self.query_items_with_attributes(attributes, start)
else:
- result = self.query_items(self.conditions, self.or_conditions,
- self.substitutions, start=start)
+ result, count = self.query_items(start=start)
# add price and availability info in results
for item in result:
@@ -78,7 +85,11 @@
discount_percent = frappe.utils.flt(fields["discount"][0])
result = [row for row in result if row.get("discount_percent") and row.discount_percent >= discount_percent]
- return result, discounts
+ return {
+ "items": result,
+ "items_count": count,
+ "discounts": discounts
+ }
def get_price_discount_info(self, item, price_object, discount_list):
"""Modify item object and add price details."""
@@ -104,51 +115,52 @@
elif not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
item.in_stock = "green" # non-stock item will always be available
- def query_items(self, conditions, or_conditions, substitutions, start=0, with_attributes=False):
+ def query_items(self, start=0):
"""Build a query to fetch Website Items based on field filters."""
- self.query_fields = ", ".join(self.fields)
+ count = frappe.db.get_all(
+ "Website Item",
+ fields=["count(*) as count"],
+ filters=self.filters,
+ or_filters=self.or_filters,
+ limit_start=start)[0].get("count")
- attribute_table = ", `tabItem Variant Attribute` iva" if with_attributes else ""
+ items = frappe.db.get_all(
+ "Website Item",
+ fields=self.fields,
+ filters=self.filters,
+ or_filters=self.or_filters,
+ limit_page_length=self.page_length,
+ limit_start=start)
- return frappe.db.sql(f"""
- select distinct {self.query_fields}
- from
- `tabWebsite Item` wi {attribute_table}
- where
- wi.published = 1
- {conditions}
- {or_conditions}
- limit {self.page_length} offset {start}
- """,
- tuple(substitutions),
- as_dict=1)
+ return items, count or 0
def query_items_with_attributes(self, attributes, start=0):
"""Build a query to fetch Website Items based on field & attribute filters."""
all_items = []
- self.conditions += " and iva.parent = wi.item_code"
+ item_codes = []
for attribute, values in attributes.items():
if not isinstance(values, list):
values = [values]
- conditions_copy = self.conditions
- substitutions_copy = self.substitutions.copy()
+ # get items that have selected attribute & value
+ item_code_list = frappe.db.get_all(
+ "Item",
+ fields=["item_code"],
+ filters=[
+ ["published_in_website", "=", 1],
+ ["Item Variant Attribute", "attribute", "=", attribute],
+ ["Item Variant Attribute", "attribute_value", "in", values]
+ ])
+ item_codes.append({x.item_code for x in item_code_list})
- conditions_copy += " and iva.attribute = '{0}' and iva.attribute_value in ({1})" \
- .format(attribute, (", ").join(['%s'] * len(values)))
- substitutions_copy.extend(values)
+ if item_codes:
+ item_codes = list(set.intersection(*item_codes))
+ self.filters.append(["item_code", "in", item_codes])
- items = self.query_items(conditions_copy, self.or_conditions, substitutions_copy,
- start=start, with_attributes=True)
+ items, count = self.query_items(start=start)
- items_dict = {item.name: item for item in items}
- # TODO: Replace Variants by their parent templates
-
- all_items.append(set(items_dict.keys()))
-
- result = [items_dict.get(item) for item in set.intersection(*all_items)]
- return result
+ return items, count
def build_fields_filters(self, filters):
"""Build filters for field values
@@ -171,11 +183,10 @@
self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
elif isinstance(values, list):
# If value is a list use `IN` query
- self.conditions += " and wi.{0} in ({1})".format(field, (', ').join(['%s'] * len(values)))
- self.substitutions.extend(values)
+ self.filters.append([field, "in", values])
else:
# `=` will be faster than `IN` for most cases
- self.conditions += " and wi.{0} = '{1}'".format(field, values)
+ self.filters.append([field, "=", values])
def build_search_filters(self, search_term):
"""Query search term in specified fields
@@ -198,4 +209,4 @@
# Build or filters for query
search = '%{}%'.format(search_term)
for field in search_fields:
- self.or_conditions += " or {0} like '{1}'".format(field, search)
+ self.or_filters.append([field, "like", search])
diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js
index 5ebf44e..67fc913 100644
--- a/erpnext/e_commerce/product_view.js
+++ b/erpnext/e_commerce/product_view.js
@@ -32,28 +32,31 @@
this.disable_view_toggler(true);
frappe.call({
- method: 'erpnext.e_commerce.doctype.website_item.website_item.get_product_filter_data',
+ method: 'erpnext.e_commerce.api.get_product_filter_data',
args: args,
callback: function(result) {
if (!result.exc && result && result.message) {
- if (me.item_group && result.message[3].length) {
- me.render_item_sub_categories(result.message[3]);
+ if (me.item_group && result.message["sub_categories"].length) {
+ me.render_item_sub_categories(result.message["sub_categories"]);
}
- if (!result.message[0].length) {
+ if (!result.message["items"].length) {
// if result has no items or result is empty
me.render_no_products_section();
+
+ me.bind_filters();
+ me.restore_filters_state();
} else {
- me.render_filters(result.message[1]);
+ me.render_filters(result.message["filters"]);
// Render views
- me.render_list_view(result.message[0], result.message[2]);
- me.render_grid_view(result.message[0], result.message[2]);
- me.products = result.message[0];
+ me.render_list_view(result.message["items"], result.message["settings"]);
+ me.render_grid_view(result.message["items"], result.message["settings"]);
+ me.products = result.message["items"];
}
// Bottom paging
- me.add_paging_section(result.message[2]);
+ me.add_paging_section(result.message["settings"]);
} else {
me.render_no_products_section();
}
@@ -190,6 +193,7 @@
$("#products-grid-area").addClass("hidden");
$("#products-list-area").removeClass("hidden");
+ localStorage.setItem("product_view", "List View");
});
$("#image-view").click(function() {
@@ -200,6 +204,7 @@
$("#products-list-area").addClass("hidden");
$("#products-grid-area").removeClass("hidden");
+ localStorage.setItem("product_view", "Grid View");
});
}
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index e38514a..db2dec0 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -5,7 +5,7 @@
let is_item_group_page = $(".item-group-content").data("item-group");
this.item_group = is_item_group_page || null;
- let view_type = "List View";
+ let view_type = localStorage.getItem("product_view") || "List View";
// Render Product Views, Filters & Search
frappe.require('/assets/js/e-commerce.min.js', function() {