brotherton-erpnext/erpnext/e_commerce/product_query.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

191 lines
6.2 KiB
Python
Raw Normal View History

2020-12-24 12:28:18 +00:00
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
2020-12-24 12:24:07 +00:00
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
2020-12-24 12:24:07 +00:00
2020-12-24 12:24:07 +00:00
class ProductQuery:
"""Query engine for product listing
2020-12-29 11:47:03 +00:00
2020-12-24 12:24:07 +00:00
Attributes:
fields (list): Fields to fetch in query
conditions (string): Conditions for query building
or_conditions (string): Search conditions
2020-12-24 12:24:07 +00:00
page_length (Int): Length of page for the query
settings (Document): E Commerce Settings DocType
2020-12-24 12:24:07 +00:00
"""
def __init__(self):
self.settings = frappe.get_doc("E Commerce Settings")
2020-12-24 12:24:07 +00:00
self.page_length = self.settings.products_per_page or 20
self.fields = ['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 = []
2020-12-24 12:24:07 +00:00
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
2020-12-24 12:28:18 +00:00
"""Summary
2020-12-29 11:47:03 +00:00
2020-12-24 12:28:18 +00:00
Args:
attributes (dict, optional): Item Attribute filters
fields (dict, optional): Field level filters
search_term (str, optional): Search term to lookup
start (int, optional): Page start
2020-12-29 11:47:03 +00:00
2020-12-24 12:28:18 +00:00
Returns:
list: List of results with set fields
"""
2020-12-24 12:24:07 +00:00
if fields: self.build_fields_filters(fields)
if search_term: self.build_search_filters(search_term)
2020-12-29 11:47:03 +00:00
if self.settings.hide_variants:
self.conditions += " and wi.variant_of is null"
2020-12-24 12:24:07 +00:00
result = []
website_item_groups = []
# if from item group page consider website item group table
if item_group:
website_item_groups = frappe.db.get_all(
"Item",
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
filters=[["Website Item Group", "item_group", "=", item_group]]
)
2020-12-24 12:24:07 +00:00
if attributes:
result = self.query_items_with_attributes(attributes, start)
2020-12-24 12:24:07 +00:00
else:
result = self.query_items(self.conditions, self.or_conditions,
self.substitutions, start=start)
2021-01-20 12:14:08 +00:00
# add price and availability info in results
2021-01-20 12:14:08 +00:00
for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
2021-02-05 09:22:31 +00:00
if product_info:
item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
item.price = (product_info.get('price') or {}).get('price_list_rate')
2020-12-24 12:24:07 +00:00
if self.settings.show_stock_availability:
if item.get("website_warehouse"):
stock_qty = frappe.utils.flt(
frappe.db.get_value("Bin",
{
"item_code": item.item_code,
"warehouse": item.get("website_warehouse")
},
"actual_qty")
)
item.in_stock = "green" if stock_qty else "red"
elif not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
item.in_stock = "green" # non-stock item will always be available
item.wished = False
if frappe.db.exists("Wishlist Items", {"item_code": item.item_code, "parent": frappe.session.user}):
item.wished = True
2020-12-24 12:24:07 +00:00
return result
def query_items(self, conditions, or_conditions, substitutions, start=0):
"""Build a query to fetch Website Items based on field filters."""
self.query_fields = (", ").join(self.fields)
return frappe.db.sql("""
select distinct {query_fields}
from
`tabWebsite Item` wi, `tabItem Variant Attribute` iva
where
wi.published = 1
{conditions}
{or_conditions}
limit {limit} offset {start}
""".format(
query_fields=self.query_fields,
conditions=conditions,
or_conditions=or_conditions,
limit=self.page_length,
start=start),
tuple(substitutions),
as_dict=1)
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"
for attribute, values in attributes.items():
if not isinstance(values, list): values = [values]
conditions_copy = self.conditions
substitutions_copy = self.substitutions.copy()
conditions_copy += " and iva.attribute = '{0}' and iva.attribute_value in ({1})" \
.format(attribute, (", ").join(['%s'] * len(values)))
substitutions_copy.extend(values)
items = self.query_items(conditions_copy, self.or_conditions, substitutions_copy, 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 list(set.intersection(*all_items))]
return result
2020-12-24 12:24:07 +00:00
def build_fields_filters(self, filters):
2020-12-24 12:28:18 +00:00
"""Build filters for field values
2020-12-29 11:47:03 +00:00
2020-12-24 12:28:18 +00:00
Args:
filters (dict): Filters
"""
2020-12-24 12:24:07 +00:00
for field, values in filters.items():
if not values:
continue
2020-12-29 11:47:03 +00:00
# handle multiselect fields in filter addition
meta = frappe.get_meta('Item', cached=True)
df = meta.get_field(field)
if df.fieldtype == 'Table MultiSelect':
child_doctype = df.options
child_meta = frappe.get_meta(child_doctype, cached=True)
fields = child_meta.get("fields")
if fields:
self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
elif isinstance(values, list):
2020-12-24 12:28:18 +00:00
# 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)
2020-12-24 12:24:07 +00:00
else:
2020-12-24 12:28:18 +00:00
# `=` will be faster than `IN` for most cases
self.conditions += " and wi.{0} = '{1}'".format(field, values)
2020-12-24 12:24:07 +00:00
def build_search_filters(self, search_term):
2020-12-24 12:28:18 +00:00
"""Query search term in specified fields
2020-12-29 11:47:03 +00:00
2020-12-24 12:28:18 +00:00
Args:
search_term (str): Search candidate
"""
2020-12-24 12:24:07 +00:00
# Default fields to search from
default_fields = {'name', 'item_name', 'description', 'item_group'}
# Get meta search fields
meta = frappe.get_meta("Item")
meta_fields = set(meta.get_search_fields())
# Join the meta fields and default fields set
search_fields = default_fields.union(meta_fields)
try:
if frappe.db.count('Item', cache=True) > 50000:
search_fields.remove('description')
except KeyError:
pass
# Build or filters for query
search = '%{}%'.format(search_term)
for field in search_fields:
self.or_conditions += " or {0} like '{1}'".format(field, search)