2020-12-24 17:58:18 +05:30
|
|
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
2020-12-24 17:54:07 +05:30
|
|
|
# License: GNU General Public License v3. See license.txt
|
|
|
|
|
|
|
|
import frappe
|
2021-09-02 16:44:59 +05:30
|
|
|
|
2021-04-20 21:54:52 +05:30
|
|
|
from frappe.utils import flt
|
|
|
|
|
2021-02-25 13:56:38 +05:30
|
|
|
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
2020-12-24 17:54:07 +05:30
|
|
|
|
2021-09-02 16:44:59 +05:30
|
|
|
|
2020-12-24 17:54:07 +05:30
|
|
|
class ProductQuery:
|
|
|
|
"""Query engine for product listing
|
2020-12-29 17:17:03 +05:30
|
|
|
|
2020-12-24 17:54:07 +05:30
|
|
|
Attributes:
|
2021-04-08 15:25:43 +05:30
|
|
|
fields (list): Fields to fetch in query
|
|
|
|
conditions (string): Conditions for query building
|
|
|
|
or_conditions (string): Search conditions
|
|
|
|
page_length (Int): Length of page for the query
|
|
|
|
settings (Document): E Commerce Settings DocType
|
2020-12-24 17:54:07 +05:30
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
2021-02-10 19:44:10 +05:30
|
|
|
self.settings = frappe.get_doc("E Commerce Settings")
|
2020-12-24 17:54:07 +05:30
|
|
|
self.page_length = self.settings.products_per_page or 20
|
2021-05-13 01:22:05 +05:30
|
|
|
self.fields = ['wi.web_item_name', 'wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of',
|
2021-02-16 18:45:36 +05:30
|
|
|
'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description',
|
2021-03-11 10:56:00 +05:30
|
|
|
'wi.route', 'wi.website_warehouse']
|
2021-02-16 18:45:36 +05:30
|
|
|
self.conditions = ""
|
|
|
|
self.or_conditions = ""
|
|
|
|
self.substitutions = []
|
2020-12-24 17:54:07 +05:30
|
|
|
|
2021-06-23 14:08:07 +05:30
|
|
|
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
|
2020-12-24 17:58:18 +05:30
|
|
|
"""Summary
|
2020-12-29 17:17:03 +05:30
|
|
|
|
2020-12-24 17:58:18 +05:30
|
|
|
Args:
|
2021-04-08 15:25:43 +05:30
|
|
|
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 17:17:03 +05:30
|
|
|
|
2020-12-24 17:58:18 +05:30
|
|
|
Returns:
|
2021-04-08 15:25:43 +05:30
|
|
|
list: List of results with set fields
|
2020-12-24 17:58:18 +05:30
|
|
|
"""
|
2021-04-20 21:54:52 +05:30
|
|
|
result, discount_list = [], []
|
|
|
|
|
2021-04-08 15:25:43 +05:30
|
|
|
if fields:
|
|
|
|
self.build_fields_filters(fields)
|
|
|
|
if search_term:
|
|
|
|
self.build_search_filters(search_term)
|
2021-02-19 15:56:52 +05:30
|
|
|
if self.settings.hide_variants:
|
|
|
|
self.conditions += " and wi.variant_of is null"
|
|
|
|
|
2020-12-24 17:54:07 +05:30
|
|
|
if attributes:
|
2021-02-16 18:45:36 +05:30
|
|
|
result = self.query_items_with_attributes(attributes, start)
|
2020-12-24 17:54:07 +05:30
|
|
|
else:
|
2021-02-16 18:45:36 +05:30
|
|
|
result = self.query_items(self.conditions, self.or_conditions,
|
|
|
|
self.substitutions, start=start)
|
2021-01-20 17:44:08 +05:30
|
|
|
|
2021-03-11 10:56:00 +05:30
|
|
|
# add price and availability info in results
|
2021-01-20 17:44:08 +05:30
|
|
|
for item in result:
|
|
|
|
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
|
2021-04-20 21:54:52 +05:30
|
|
|
|
2021-04-13 00:39:26 +05:30
|
|
|
if product_info and product_info['price']:
|
2021-04-20 21:54:52 +05:30
|
|
|
self.get_price_discount_info(item, product_info['price'], discount_list)
|
2020-12-24 17:54:07 +05:30
|
|
|
|
2021-03-25 11:52:50 +05:30
|
|
|
if self.settings.show_stock_availability:
|
2021-04-20 21:54:52 +05:30
|
|
|
self.get_stock_availability(item)
|
2021-03-14 17:28:49 +05:30
|
|
|
|
|
|
|
item.wished = False
|
2021-03-16 00:05:53 +05:30
|
|
|
if frappe.db.exists("Wishlist Items", {"item_code": item.item_code, "parent": frappe.session.user}):
|
2021-03-14 17:28:49 +05:30
|
|
|
item.wished = True
|
|
|
|
|
2021-04-20 21:54:52 +05:30
|
|
|
discounts = []
|
|
|
|
if discount_list:
|
|
|
|
discounts = [min(discount_list), max(discount_list)]
|
|
|
|
|
|
|
|
if fields and "discount" in fields:
|
|
|
|
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
|
|
|
|
|
|
|
|
def get_price_discount_info(self, item, price_object, discount_list):
|
|
|
|
"""Modify item object and add price details."""
|
|
|
|
item.formatted_mrp = price_object.get('formatted_mrp')
|
|
|
|
item.formatted_price = price_object.get('formatted_price')
|
|
|
|
|
|
|
|
if price_object.get('discount_percent'):
|
|
|
|
item.discount_percent = flt(price_object.discount_percent)
|
|
|
|
discount_list.append(price_object.discount_percent)
|
|
|
|
|
|
|
|
if item.formatted_mrp:
|
|
|
|
item.discount = price_object.get('formatted_discount_percent') or \
|
|
|
|
price_object.get('formatted_discount_rate')
|
|
|
|
item.price = price_object.get('price_list_rate')
|
|
|
|
|
|
|
|
def get_stock_availability(self, item):
|
|
|
|
"""Modify item object and add stock details."""
|
|
|
|
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
|
2020-12-24 17:54:07 +05:30
|
|
|
|
2021-05-20 01:30:01 +05:30
|
|
|
def query_items(self, conditions, or_conditions, substitutions, start=0, with_attributes=False):
|
2021-02-16 18:45:36 +05:30
|
|
|
"""Build a query to fetch Website Items based on field filters."""
|
2021-05-25 01:35:22 +05:30
|
|
|
self.query_fields = ", ".join(self.fields)
|
2021-02-19 15:56:52 +05:30
|
|
|
|
2021-05-20 01:30:01 +05:30
|
|
|
attribute_table = ", `tabItem Variant Attribute` iva" if with_attributes else ""
|
|
|
|
|
|
|
|
return frappe.db.sql(f"""
|
|
|
|
select distinct {self.query_fields}
|
2021-02-16 18:45:36 +05:30
|
|
|
from
|
2021-05-20 01:30:01 +05:30
|
|
|
`tabWebsite Item` wi {attribute_table}
|
2021-02-16 18:45:36 +05:30
|
|
|
where
|
|
|
|
wi.published = 1
|
|
|
|
{conditions}
|
|
|
|
{or_conditions}
|
2021-05-20 01:30:01 +05:30
|
|
|
limit {self.page_length} offset {start}
|
2021-05-25 01:35:22 +05:30
|
|
|
""",
|
|
|
|
tuple(substitutions),
|
|
|
|
as_dict=1)
|
2021-02-16 18:45:36 +05:30
|
|
|
|
|
|
|
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():
|
2021-04-08 15:25:43 +05:30
|
|
|
if not isinstance(values, list):
|
|
|
|
values = [values]
|
2021-02-16 18:45:36 +05:30
|
|
|
|
|
|
|
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)
|
|
|
|
|
2021-05-20 01:30:01 +05:30
|
|
|
items = self.query_items(conditions_copy, self.or_conditions, substitutions_copy,
|
|
|
|
start=start, with_attributes=True)
|
2021-02-16 18:45:36 +05:30
|
|
|
|
|
|
|
items_dict = {item.name: item for item in items}
|
|
|
|
# TODO: Replace Variants by their parent templates
|
|
|
|
|
|
|
|
all_items.append(set(items_dict.keys()))
|
|
|
|
|
2021-05-25 01:35:22 +05:30
|
|
|
result = [items_dict.get(item) for item in set.intersection(*all_items)]
|
2021-02-16 18:45:36 +05:30
|
|
|
return result
|
|
|
|
|
2020-12-24 17:54:07 +05:30
|
|
|
def build_fields_filters(self, filters):
|
2020-12-24 17:58:18 +05:30
|
|
|
"""Build filters for field values
|
2020-12-29 17:17:03 +05:30
|
|
|
|
2020-12-24 17:58:18 +05:30
|
|
|
Args:
|
2021-04-08 15:25:43 +05:30
|
|
|
filters (dict): Filters
|
2020-12-24 17:58:18 +05:30
|
|
|
"""
|
2020-12-24 17:54:07 +05:30
|
|
|
for field, values in filters.items():
|
2021-04-20 21:54:52 +05:30
|
|
|
if not values or field == "discount":
|
2020-12-24 17:54:07 +05:30
|
|
|
continue
|
2020-12-29 17:17:03 +05:30
|
|
|
|
2021-06-23 20:06:11 +05:30
|
|
|
# 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)
|
2021-06-23 22:38:10 +05:30
|
|
|
fields = child_meta.get("fields")
|
2021-06-23 20:06:11 +05:30
|
|
|
if fields:
|
|
|
|
self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
|
2021-06-23 22:38:10 +05:30
|
|
|
elif isinstance(values, list):
|
2020-12-24 17:58:18 +05:30
|
|
|
# If value is a list use `IN` query
|
2021-02-16 18:45:36 +05:30
|
|
|
self.conditions += " and wi.{0} in ({1})".format(field, (', ').join(['%s'] * len(values)))
|
|
|
|
self.substitutions.extend(values)
|
2020-12-24 17:54:07 +05:30
|
|
|
else:
|
2020-12-24 17:58:18 +05:30
|
|
|
# `=` will be faster than `IN` for most cases
|
2021-02-16 18:45:36 +05:30
|
|
|
self.conditions += " and wi.{0} = '{1}'".format(field, values)
|
2020-12-24 17:54:07 +05:30
|
|
|
|
|
|
|
def build_search_filters(self, search_term):
|
2020-12-24 17:58:18 +05:30
|
|
|
"""Query search term in specified fields
|
2020-12-29 17:17:03 +05:30
|
|
|
|
2020-12-24 17:58:18 +05:30
|
|
|
Args:
|
2021-04-08 15:25:43 +05:30
|
|
|
search_term (str): Search candidate
|
2020-12-24 17:58:18 +05:30
|
|
|
"""
|
2020-12-24 17:54:07 +05:30
|
|
|
# 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)
|
2021-05-25 01:35:22 +05:30
|
|
|
if frappe.db.count('Item', cache=True) > 50000:
|
|
|
|
search_fields.discard('description')
|
2020-12-24 17:54:07 +05:30
|
|
|
|
|
|
|
# Build or filters for query
|
|
|
|
search = '%{}%'.format(search_term)
|
2021-02-16 18:45:36 +05:30
|
|
|
for field in search_fields:
|
|
|
|
self.or_conditions += " or {0} like '{1}'".format(field, search)
|