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
This commit is contained in:
parent
cb52c98f02
commit
0dadf535c1
49
erpnext/e_commerce/api.py
Normal file
49
erpnext/e_commerce/api.py
Normal file
@ -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"]
|
||||
}
|
@ -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 frappe.website.doctype.website_slideshow.website_slideshow import get_slide
|
||||
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 @@ def make_website_item(doc, save=True):
|
||||
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 @@ def make_website_item(doc, save=True):
|
||||
|
||||
# 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 @@ def check_if_user_is_customer(user=None):
|
||||
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
|
||||
return True if customer else False
|
@ -95,13 +95,16 @@ class TestProductConfigurator(unittest.TestCase):
|
||||
|
||||
# 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):
|
||||
|
@ -18,16 +18,14 @@ class ProductQuery:
|
||||
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 @@ class ProductQuery:
|
||||
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 @@ class ProductQuery:
|
||||
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 @@ class ProductQuery:
|
||||
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 @@ class ProductQuery:
|
||||
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 @@ class ProductQuery:
|
||||
# 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])
|
||||
|
@ -32,28 +32,31 @@ erpnext.ProductView = class {
|
||||
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 @@ erpnext.ProductView = class {
|
||||
|
||||
$("#products-grid-area").addClass("hidden");
|
||||
$("#products-list-area").removeClass("hidden");
|
||||
localStorage.setItem("product_view", "List View");
|
||||
});
|
||||
|
||||
$("#image-view").click(function() {
|
||||
@ -200,6 +204,7 @@ erpnext.ProductView = class {
|
||||
|
||||
$("#products-list-area").addClass("hidden");
|
||||
$("#products-grid-area").removeClass("hidden");
|
||||
localStorage.setItem("product_view", "Grid View");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user