test: Product Query & Filter Engine, Item Group Page
- Test for ProductQuery engine and ProductFilters engine
- Test for engine for Item Group too
- Renamed ‘product_configurator’ to ‘variant_selector’
- Cleaned up filters.py
- Modal freeze backdrop lighter only in cart, since there’s nothing over it
- Fixed unusual spacing in variant selector dialog
- Made `get_child_groups_for_website` more readable
- Replaced ‘Configure’ with ‘Select’ for variant selection
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index f8109ab..68ad702 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -132,7 +132,7 @@
conditions = " or ".join(conditions)
- from erpnext.e_commerce.product_configurator.utils import get_item_codes_by_attributes
+ from erpnext.e_commerce.variant_selector.utils import get_item_codes_by_attributes
possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code]
for variant in possible_variants:
diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py
index 728d336..01bde29 100644
--- a/erpnext/e_commerce/api.py
+++ b/erpnext/e_commerce/api.py
@@ -8,11 +8,22 @@
from erpnext.e_commerce.product_data_engine.query import ProductQuery
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
-from erpnext.setup.doctype.item_group.item_group import get_child_groups
+from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
@frappe.whitelist(allow_guest=True)
def get_product_filter_data(query_args=None):
- """Get filtered products and discount filters."""
+ """
+ Returns filtered products and discount filters.
+ :param query_args (dict): contains filters to get products list
+
+ Query Args filters:
+ search (str): Search Term.
+ field_filters (dict): Keys include item_group, brand, etc.
+ attribute_filters(dict): Keys include Color, Size, etc.
+ start (int): Offset items by
+ item_group (str): Valid Item Group
+ from_filters (bool): Set as True to jump to page 1
+ """
if isinstance(query_args, str):
query_args = json.loads(query_args)
@@ -35,7 +46,7 @@
sub_categories = []
if item_group:
field_filters['item_group'] = item_group
- sub_categories = get_child_groups(item_group)
+ sub_categories = get_child_groups_for_website(item_group, immediate=True)
engine = ProductQuery()
try:
@@ -46,7 +57,7 @@
start=start,
item_group=item_group
)
- except Exception as e:
+ except Exception:
traceback = frappe.get_traceback()
frappe.log_error(traceback, frappe._("Product Engine Error"))
return {"exc": "Something went wrong!"}
diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py
index 9afca25..4a8e820 100644
--- a/erpnext/e_commerce/doctype/website_item/test_website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -26,6 +26,10 @@
"price_list": "_Test Price List India"
})
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.rollback()
+
def setUp(self):
if self._testMethodName in WEBITEM_DESK_TESTS:
make_item("Test Web Item", {
@@ -38,22 +42,13 @@
]
})
elif self._testMethodName in WEBITEM_PRICE_TESTS:
- self.create_regular_web_item()
+ create_regular_web_item()
make_web_item_price(item_code="Test Mobile Phone")
make_web_pricing_rule(
title="Test Pricing Rule for Test Mobile Phone",
item_code="Test Mobile Phone",
selling=1)
- def tearDown(self):
- if self._testMethodName in WEBITEM_DESK_TESTS:
- frappe.get_doc("Item", "Test Web Item").delete()
- elif self._testMethodName in WEBITEM_PRICE_TESTS:
- frappe.delete_doc("Pricing Rule", "Test Pricing Rule for Test Mobile Phone")
- frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone"}).delete()
- frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
-
-
def test_index_creation(self):
"Check if index is getting created in db."
from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update
@@ -105,6 +100,8 @@
item.reload()
self.assertEqual(item.published_in_website, 0)
+ item.delete()
+
def test_publish_variant_and_template(self):
"Check if template is published on publishing variant."
# template "Test Web Item" created on setUp
@@ -256,7 +253,7 @@
2) Showing stock availability disabled
"""
item_code = "Test Mobile Phone"
- self.create_regular_web_item()
+ create_regular_web_item()
setup_e_commerce_settings({"show_stock_availability": 1})
frappe.local.shopping_cart_settings = None
@@ -298,7 +295,7 @@
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item_code = "Test Mobile Phone"
- self.create_regular_web_item()
+ create_regular_web_item()
setup_e_commerce_settings({"show_stock_availability": 1})
frappe.local.shopping_cart_settings = None
@@ -339,7 +336,7 @@
def test_recommended_item(self):
"Check if added recommended items are fetched correctly."
item_code = "Test Mobile Phone"
- web_item = self.create_regular_web_item(item_code)
+ web_item = create_regular_web_item(item_code)
setup_e_commerce_settings({
"enable_recommendations": 1,
@@ -347,7 +344,7 @@
})
# create recommended web item and price for it
- recommended_web_item = self.create_regular_web_item("Test Mobile Phone 1")
+ recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
make_web_item_price(item_code="Test Mobile Phone 1")
# add recommended item to first web item
@@ -379,14 +376,14 @@
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched
# tear down
- frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone 1"}).delete()
web_item.delete()
recommended_web_item.delete()
+ frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
def test_recommended_item_for_guest_user(self):
"Check if added recommended items are fetched correctly for guest user."
item_code = "Test Mobile Phone"
- web_item = self.create_regular_web_item(item_code)
+ web_item = create_regular_web_item(item_code)
# price visible to guests
setup_e_commerce_settings({
@@ -396,7 +393,7 @@
})
# create recommended web item and price for it
- recommended_web_item = self.create_regular_web_item("Test Mobile Phone 1")
+ recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
make_web_item_price(item_code="Test Mobile Phone 1")
# add recommended item to first web item
@@ -428,22 +425,24 @@
# tear down
frappe.set_user("Administrator")
- frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone 1"}).delete()
web_item.delete()
recommended_web_item.delete()
+ frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
- def create_regular_web_item(self, item_code=None):
- "Create Regular Item and Website Item."
- item_code = item_code or "Test Mobile Phone"
- item = make_item(item_code)
+def create_regular_web_item(item_code=None, item_args=None, web_args=None):
+ "Create Regular Item and Website Item."
+ item_code = item_code or "Test Mobile Phone"
+ item = make_item(item_code, properties=item_args)
- if not frappe.db.exists("Website Item", {"item_code": item_code}):
- web_item = make_website_item(item, save=False)
- web_item.save()
- else:
- web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
+ if not frappe.db.exists("Website Item", {"item_code": item_code}):
+ web_item = make_website_item(item, save=False)
+ if web_args:
+ web_item.update(web_args)
+ web_item.save()
+ else:
+ web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
- return web_item
+ return web_item
def make_web_item_price(**kwargs):
item_code = kwargs.get("item_code")
diff --git a/erpnext/e_commerce/product_configurator/test_product_configurator.py b/erpnext/e_commerce/product_configurator/test_product_configurator.py
deleted file mode 100644
index fe4ef08..0000000
--- a/erpnext/e_commerce/product_configurator/test_product_configurator.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import frappe, unittest
-from erpnext.e_commerce.product_data_engine.query import ProductQuery
-from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
-
-test_dependencies = ["Item"]
-#TODO: Rename to test item variant configurator
-
-class TestProductConfigurator(unittest.TestCase):
- def setUp(self):
- self.create_variant_item()
- self.publish_items_on_website()
-
- # TODO: E-commerce server side tests
- # def test_product_list(self):
- # template_items = frappe.get_all('Item', {'show_in_website': 1})
- # variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
-
- # products_settings = frappe.get_doc('Products Settings')
- # products_settings.enable_field_filters = 1
- # products_settings.append('filter_fields', {'fieldname': 'item_group'})
- # products_settings.append('filter_fields', {'fieldname': 'stock_uom'})
- # products_settings.save()
-
- # html = get_html_for_route('all-products')
-
- # soup = BeautifulSoup(html, 'html.parser')
- # products_list = soup.find(class_='products-list')
- # items = products_list.find_all(class_='card')
- # self.assertEqual(len(items), len(template_items + variant_items))
-
- # items_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_in_website': 1})
- # variants_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_variant_in_website': 1})
-
- # # mock query params
- # frappe.form_dict = frappe._dict({
- # 'field_filters': '{"item_group":["_Test Item Group Desktops"]}'
- # })
- # html = get_html_for_route('all-products')
- # soup = BeautifulSoup(html, 'html.parser')
- # products_list = soup.find(class_='products-list')
- # items = products_list.find_all(class_='card')
- # self.assertEqual(len(items), len(items_with_item_group + variants_with_item_group))
-
-
- # def test_get_products_for_website(self):
- # items = get_products_for_website(attribute_filters={
- # 'Test Size': ['2XL']
- # })
- # self.assertEqual(len(items), 1)
-
- # def test_products_in_multiple_item_groups(self):
- # """Check if product is visible on multiple item group pages barring its own."""
- # from erpnext.shopping_cart.product_query import ProductQuery
-
- # if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
- # item_group_doc = frappe.get_doc({
- # "doctype": "Item Group",
- # "item_group_name": "Tech Items",
- # "parent_item_group": "All Item Groups",
- # "show_in_website": 1
- # }).insert()
- # else:
- # item_group_doc = frappe.get_doc("Item Group", "Tech Items")
-
- # doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
- # if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
- # doc.append("website_item_groups", {
- # "item_group": "_Test Item Group Desktops"
- # })
- # doc.save()
-
- # # check if item is visible in its own Item Group's page
- # engine = ProductQuery()
- # 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()
- # 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]
-
- # self.assertIn(len(items), [2, 3])
- # self.assertIn("Portal Item", item_codes)
-
- # # teardown
- # doc.delete()
- # item_group_doc.delete()
diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py
index 75137a7..7ae87eb 100644
--- a/erpnext/e_commerce/product_data_engine/filters.py
+++ b/erpnext/e_commerce/product_data_engine/filters.py
@@ -6,7 +6,7 @@
class ProductFiltersBuilder:
def __init__(self, item_group=None):
- if not item_group or item_group == "E Commerce Settings":
+ if not item_group:
self.doc = frappe.get_doc("E Commerce Settings")
else:
self.doc = frappe.get_doc("Item Group", item_group)
@@ -17,36 +17,39 @@
if not self.item_group and not self.doc.enable_field_filters:
return
- filter_fields = [row.fieldname for row in self.doc.filter_fields]
+ fields, filter_data = [], []
+ filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
- meta = frappe.get_meta('Item')
- fields = [df for df in meta.fields if df.fieldname in filter_fields]
+ # filter valid field filters i.e. those that exist in Item
+ item_meta = frappe.get_meta('Item', cached=True)
+ fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
- filter_data = []
for df in fields:
- filters, or_filters = {}, []
+ item_filters, item_or_filters = {}, []
+ link_doctype_values = self.get_filtered_link_doctype_records(df)
+
if df.fieldtype == "Link":
if self.item_group:
- or_filters.extend([
+ item_or_filters.extend([
["item_group", "=", self.item_group],
- ["Website Item Group", "item_group", "=", self.item_group]
+ ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
])
- values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
+ # Get link field values attached to published items
+ item_filters['published_in_website'] = 1
+ item_values = frappe.get_all(
+ "Item",
+ fields=[df.fieldname],
+ filters=item_filters,
+ or_filters=item_or_filters,
+ distinct="True",
+ pluck=df.fieldname
+ )
+
+ values = list(set(item_values) & link_doctype_values) # intersection of both
else:
- doctype = df.get_link_doctype()
-
- # apply enable/disable/show_in_website filter
- meta = frappe.get_meta(doctype)
-
- if meta.has_field('enabled'):
- filters['enabled'] = 1
- if meta.has_field('disabled'):
- filters['disabled'] = 0
- if meta.has_field('show_in_website'):
- filters['show_in_website'] = 1
-
- values = [d.name for d in frappe.get_all(doctype, filters)]
+ # table multiselect
+ values = list(link_doctype_values)
# Remove None
if None in values:
@@ -57,6 +60,36 @@
return filter_data
+ def get_filtered_link_doctype_records(self, field):
+ """
+ Get valid link doctype records depending on filters.
+ Apply enable/disable/show_in_website filter.
+ Returns:
+ set: A set containing valid record names
+ """
+ link_doctype = field.get_link_doctype()
+ meta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None
+ if meta:
+ filters = self.get_link_doctype_filters(meta)
+ link_doctype_values = set(d.name for d in frappe.get_all(link_doctype, filters))
+
+ return link_doctype_values if meta else set()
+
+ def get_link_doctype_filters(self, meta):
+ "Filters for Link Doctype eg. 'show_in_website'."
+ filters = {}
+ if not meta:
+ return filters
+
+ if meta.has_field('enabled'):
+ filters['enabled'] = 1
+ if meta.has_field('disabled'):
+ filters['disabled'] = 0
+ if meta.has_field('show_in_website'):
+ filters['show_in_website'] = 1
+
+ return filters
+
def get_attribute_filters(self):
if not self.item_group and not self.doc.enable_attribute_filters:
return
@@ -92,9 +125,9 @@
def get_discount_filters(self, discounts):
discount_filters = []
- # [25.89, 60.5]
+ # [25.89, 60.5] min max
min_discount, max_discount = discounts[0], discounts[1]
- # [25, 60]
+ # [25, 60] rounded min max
min_range_absolute, max_range_absolute = floor(min_discount), floor(max_discount)
min_range = int(min_discount - (min_range_absolute % 10)) # 20
max_range = int(max_discount - (max_range_absolute % 10)) # 60
diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
new file mode 100644
index 0000000..264fbd8
--- /dev/null
+++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
@@ -0,0 +1,116 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import unittest
+
+from erpnext.e_commerce.api import get_product_filter_data
+from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
+
+test_dependencies = ["Item", "Item Group"]
+
+class TestItemGroupProductDataEngine(unittest.TestCase):
+ "Test Products & Sub-Category Querying for Product Listing on Item Group Page."
+
+ @classmethod
+ def setUpClass(cls):
+ item_codes = [
+ ("Test Mobile A", "_Test Item Group B"),
+ ("Test Mobile B", "_Test Item Group B"),
+ ("Test Mobile C", "_Test Item Group B - 1"),
+ ("Test Mobile D", "_Test Item Group B - 1"),
+ ("Test Mobile E", "_Test Item Group B - 2")
+ ]
+ for item in item_codes:
+ item_code = item[0]
+ item_args = {"item_group": item[1]}
+ if not frappe.db.exists("Website Item", {"item_code": item_code}):
+ create_regular_web_item(item_code, item_args=item_args)
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.rollback()
+
+ def test_product_listing_in_item_group(self):
+ "Test if only products belonging to the Item Group are fetched."
+ result = get_product_filter_data(query_args={
+ "field_filters": {},
+ "attribute_filters": {},
+ "start": 0,
+ "item_group": "_Test Item Group B"
+ })
+
+ items = result.get("items")
+ item_codes = [item.get("item_code") for item in items]
+
+ self.assertEqual(len(items), 2)
+ self.assertIn("Test Mobile A", item_codes)
+ self.assertNotIn("Test Mobile C", item_codes)
+
+ def test_products_in_multiple_item_groups(self):
+ """Test if product is visible on multiple item group pages barring its own."""
+ website_item = frappe.get_doc("Website Item", {"item_code": "Test Mobile E"})
+
+ # show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well
+ website_item.append("website_item_groups", {
+ "item_group": "_Test Item Group B - 1"
+ })
+ website_item.save()
+
+ result = get_product_filter_data(query_args={
+ "field_filters": {},
+ "attribute_filters": {},
+ "start": 0,
+ "item_group": "_Test Item Group B - 1"
+ })
+
+ items = result.get("items")
+ item_codes = [item.get("item_code") for item in items]
+
+ self.assertEqual(len(items), 3)
+ self.assertIn("Test Mobile E", item_codes) # visible in other item groups
+ self.assertIn("Test Mobile C", item_codes)
+ self.assertIn("Test Mobile D", item_codes)
+
+ result = get_product_filter_data(query_args={
+ "field_filters": {},
+ "attribute_filters": {},
+ "start": 0,
+ "item_group": "_Test Item Group B - 2"
+ })
+
+ items = result.get("items")
+
+ self.assertEqual(len(items), 1)
+ self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group
+
+ def test_item_group_with_sub_groups(self):
+ "Test Valid Sub Item Groups in Item Group Page."
+ frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
+ frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
+
+ result = get_product_filter_data(query_args={
+ "field_filters": {},
+ "attribute_filters": {},
+ "start": 0,
+ "item_group": "_Test Item Group B"
+ })
+
+ self.assertTrue(bool(result.get("sub_categories")))
+
+ child_groups = [d.name for d in result.get("sub_categories")]
+ # check if child group is fetched if shown in website
+ self.assertIn("_Test Item Group B - 1", child_groups)
+
+ frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
+ result = get_product_filter_data(query_args={
+ "field_filters": {},
+ "attribute_filters": {},
+ "start": 0,
+ "item_group": "_Test Item Group B"
+ })
+ child_groups = [d.name for d in result.get("sub_categories")]
+
+ # check if child group is fetched if shown in website
+ self.assertIn("_Test Item Group B - 1", child_groups)
+ self.assertIn("_Test Item Group B - 2", child_groups)
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
index 8bca046..9a7cb3c 100644
--- a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
+++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
@@ -2,37 +2,337 @@
# For license information, please see license.txt
import frappe
+import unittest
-test_dependencies = ["Item"]
+from erpnext.e_commerce.product_data_engine.query import ProductQuery
+from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
+from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
+from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import setup_e_commerce_settings
+
+test_dependencies = ["Item", "Item Group"]
class TestProductDataEngine(unittest.TestCase):
- "Test Products Querying for Product Listing."
- def test_product_list_ordering(self):
- "Check if website items appear by ranking."
- pass
+ "Test Products Querying and Filters for Product Listing."
- def test_product_list_paging(self):
- pass
+ @classmethod
+ def setUpClass(cls):
+ item_codes = [
+ ("Test 11I Laptop", "Products"), # rank 1
+ ("Test 12I Laptop", "Products"), # rank 2
+ ("Test 13I Laptop", "Products"), # rank 3
+ ("Test 14I Laptop", "Raw Material"), # rank 4
+ ("Test 15I Laptop", "Raw Material"), # rank 5
+ ("Test 16I Laptop", "Raw Material"), # rank 6
+ ("Test 17I Laptop", "Products") # rank 7
+ ]
+ for index, item in enumerate(item_codes, start=1):
+ item_code = item[0]
+ item_args = {"item_group": item[1]}
+ web_args = {"ranking": index}
+ if not frappe.db.exists("Website Item", {"item_code": item_code}):
+ create_regular_web_item(item_code, item_args=item_args, web_args=web_args)
+
+ setup_e_commerce_settings({
+ "products_per_page": 4,
+ "enable_field_filters": 1,
+ "filter_fields": [{"fieldname": "item_group"}],
+ "enable_attribute_filters": 1,
+ "filter_attributes": [{"attribute": "Test Size"}]
+ })
+ frappe.local.shopping_cart_settings = None
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.rollback()
+
+ def test_product_list_ordering_and_paging(self):
+ "Test if website items appear by ranking on different pages."
+ engine = ProductQuery()
+ result = engine.query(
+ attributes={},
+ fields={},
+ search_term=None,
+ start=0,
+ item_group=None
+ )
+ items = result.get("items")
+
+ self.assertIsNotNone(items)
+ self.assertEqual(len(items), 4)
+ self.assertGreater(result.get("items_count"), 4)
+
+ # check if items appear as per ranking set in setUpClass
+ self.assertEqual(items[0].get("item_code"), "Test 17I Laptop")
+ self.assertEqual(items[1].get("item_code"), "Test 16I Laptop")
+ self.assertEqual(items[2].get("item_code"), "Test 15I Laptop")
+ self.assertEqual(items[3].get("item_code"), "Test 14I Laptop")
+
+ # check next page
+ result = engine.query(
+ attributes={},
+ fields={},
+ search_term=None,
+ start=4,
+ item_group=None
+ )
+ items = result.get("items")
+
+ # check if items appear as per ranking set in setUpClass on next page
+ self.assertEqual(items[0].get("item_code"), "Test 13I Laptop")
+ self.assertEqual(items[1].get("item_code"), "Test 12I Laptop")
+ self.assertEqual(items[2].get("item_code"), "Test 11I Laptop")
+
+ def test_change_product_ranking(self):
+ "Test if item on second page appear on first if ranking is changed."
+ item_code = "Test 12I Laptop"
+ old_ranking = frappe.db.get_value("Website Item", {"item_code": item_code}, "ranking")
+
+ # low rank, appears on second page
+ self.assertEqual(old_ranking, 2)
+
+ # set ranking as highest rank
+ frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", 10)
+
+ engine = ProductQuery()
+ result = engine.query(
+ attributes={},
+ fields={},
+ search_term=None,
+ start=0,
+ item_group=None
+ )
+ items = result.get("items")
+
+ # check if item is the first item on the first page
+ self.assertEqual(items[0].get("item_code"), item_code)
+ self.assertEqual(items[1].get("item_code"), "Test 17I Laptop")
+
+ # tear down
+ frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", old_ranking)
+
+ def test_product_list_field_filter_builder(self):
+ "Test if field filters are fetched correctly."
+ frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 0)
+
+ filter_engine = ProductFiltersBuilder()
+ field_filters = filter_engine.get_field_filters()
+
+ # Web Items belonging to 'Products' and 'Raw Material' are available
+ # but only 'Products' has 'show_in_website' enabled
+ item_group_filters = field_filters[0]
+ docfield = item_group_filters[0]
+ valid_item_groups = item_group_filters[1]
+
+ self.assertEqual(docfield.options, "Item Group")
+ self.assertIn("Products", valid_item_groups)
+ self.assertNotIn("Raw Material", valid_item_groups)
+
+ frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 1)
+ field_filters = filter_engine.get_field_filters()
+
+ #'Products' and 'Raw Materials' both have 'show_in_website' enabled
+ item_group_filters = field_filters[0]
+ docfield = item_group_filters[0]
+ valid_item_groups = item_group_filters[1]
+
+ self.assertEqual(docfield.options, "Item Group")
+ self.assertIn("Products", valid_item_groups)
+ self.assertIn("Raw Material", valid_item_groups)
def test_product_list_with_field_filter(self):
- pass
+ "Test if field filters are applied correctly."
+ field_filters = {"item_group": "Raw Material"}
+
+ engine = ProductQuery()
+ result = engine.query(
+ attributes={},
+ fields=field_filters,
+ search_term=None,
+ start=0,
+ item_group=None
+ )
+ items = result.get("items")
+
+ # check if only 'Raw Material' are fetched in the right order
+ self.assertEqual(len(items), 3)
+ self.assertEqual(items[0].get("item_code"), "Test 16I Laptop")
+ self.assertEqual(items[1].get("item_code"), "Test 15I Laptop")
+
+ # def test_product_list_with_field_filter_table_multiselect(self):
+ # TODO
+ # pass
+
+ def test_product_list_attribute_filter_builder(self):
+ "Test if attribute filters are fetched correctly."
+ create_variant_web_item()
+
+ filter_engine = ProductFiltersBuilder()
+ attribute_filter = filter_engine.get_attribute_filters()[0]
+ attribute = attribute_filter.item_attribute_values[0]
+
+ self.assertEqual(attribute_filter.name, "Test Size")
+ self.assertEqual(len(attribute_filter.item_attribute_values), 1)
+ self.assertEqual(attribute.attribute_value, "Large")
def test_product_list_with_attribute_filter(self):
- pass
+ "Test if attribute filters are applied correctly."
+ create_variant_web_item()
- def test_product_list_with_discount_filter(self):
- pass
+ attribute_filters = {"Test Size": ["Large"]}
+ engine = ProductQuery()
+ result = engine.query(
+ attributes=attribute_filters,
+ fields={},
+ search_term=None,
+ start=0,
+ item_group=None
+ )
+ items = result.get("items")
- def test_product_list_with_mixed_filtes(self):
- pass
+ # check if only items with Test Size 'Large' are fetched
+ self.assertEqual(len(items), 1)
+ self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
- def test_product_list_with_mixed_filtes_item_group(self):
- pass
+ def test_product_list_discount_filter_builder(self):
+ "Test if discount filters are fetched correctly."
+ from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price, make_web_pricing_rule
- def test_products_in_multiple_item_groups(self):
- "Check if product is visible on multiple item group pages barring its own."
- pass
+ item_code = "Test 12I Laptop"
+ make_web_item_price(item_code=item_code)
+ make_web_pricing_rule(
+ title=f"Test Pricing Rule for {item_code}",
+ item_code=item_code,
+ selling=1
+ )
+
+ setup_e_commerce_settings({"show_price": 1})
+ frappe.local.shopping_cart_settings = None
+
+ engine = ProductQuery()
+ result = engine.query(
+ attributes={},
+ fields={},
+ search_term=None,
+ start=4,
+ item_group=None
+ )
+
+ self.assertTrue(bool(result.get("discounts")))
+
+ filter_engine = ProductFiltersBuilder()
+ discount_filters = filter_engine.get_discount_filters(result["discounts"])
+
+ self.assertEqual(len(discount_filters[0]), 2)
+ self.assertEqual(discount_filters[0][0], 10)
+ self.assertEqual(discount_filters[0][1], "10% and above")
+
+ def test_product_list_with_discount_filters(self):
+ "Test if discount filters are applied correctly."
+ from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price, make_web_pricing_rule
+
+ field_filters = {"discount": [10]}
+
+ make_web_item_price(item_code="Test 12I Laptop")
+ make_web_pricing_rule(
+ title="Test Pricing Rule for Test 12I Laptop", # 10% discount
+ item_code="Test 12I Laptop",
+ selling=1
+ )
+ make_web_item_price(item_code="Test 13I Laptop")
+ make_web_pricing_rule(
+ title="Test Pricing Rule for Test 13I Laptop", # 15% discount
+ item_code="Test 13I Laptop",
+ discount_percentage=15,
+ selling=1
+ )
+
+ setup_e_commerce_settings({"show_price": 1})
+ frappe.local.shopping_cart_settings = None
+
+ engine = ProductQuery()
+ result = engine.query(
+ attributes={},
+ fields=field_filters,
+ search_term=None,
+ start=0,
+ item_group=None
+ )
+ items = result.get("items")
+
+ # check if only product with 10% and above discount are fetched in the right order
+ self.assertEqual(len(items), 2)
+ self.assertEqual(items[0].get("item_code"), "Test 13I Laptop")
+ self.assertEqual(items[1].get("item_code"), "Test 12I Laptop")
+
+ def test_product_list_with_api(self):
+ "Test products listing using API."
+ from erpnext.e_commerce.api import get_product_filter_data
+
+ create_variant_web_item()
+
+ result = get_product_filter_data(query_args={
+ "field_filters": {
+ "item_group": "Products"
+ },
+ "attribute_filters": {
+ "Test Size": ["Large"]
+ },
+ "start": 0
+ })
+
+ items = result.get("items")
+
+ self.assertEqual(len(items), 1)
+ self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
def test_product_list_with_variants(self):
- pass
+ "Test if variants are hideen on hiding variants in settings."
+ create_variant_web_item()
+ setup_e_commerce_settings({
+ "enable_attribute_filters": 0,
+ "hide_variants": 1
+ })
+ frappe.local.shopping_cart_settings = None
+
+ attribute_filters = {"Test Size": ["Large"]}
+ engine = ProductQuery()
+ result = engine.query(
+ attributes=attribute_filters,
+ fields={},
+ search_term=None,
+ start=0,
+ item_group=None
+ )
+ items = result.get("items")
+
+ # check if any variants are fetched even though published variant exists
+ self.assertEqual(len(items), 0)
+
+ # tear down
+ setup_e_commerce_settings({
+ "enable_attribute_filters": 1,
+ "hide_variants": 0
+ })
+
+def create_variant_web_item():
+ "Create Variant and Template Website Items."
+ from erpnext.stock.doctype.item.test_item import make_item
+ from erpnext.controllers.item_variant import create_variant
+ from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+
+ make_item("Test Web Item", {
+ "has_variant": 1,
+ "variant_based_on": "Item Attribute",
+ "attributes": [
+ {
+ "attribute": "Test Size"
+ }
+ ]
+ })
+ if not frappe.db.exists("Item", "Test Web Item-L"):
+ variant = create_variant("Test Web Item", {"Test Size": "Large"})
+ variant.save()
+
+ if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}):
+ make_website_item(variant, save=True)
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_configurator/__init__.py b/erpnext/e_commerce/variant_selector/__init__.py
similarity index 100%
rename from erpnext/e_commerce/product_configurator/__init__.py
rename to erpnext/e_commerce/variant_selector/__init__.py
diff --git a/erpnext/e_commerce/product_configurator/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
similarity index 100%
rename from erpnext/e_commerce/product_configurator/item_variants_cache.py
rename to erpnext/e_commerce/variant_selector/item_variants_cache.py
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
new file mode 100644
index 0000000..3eeca17
--- /dev/null
+++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py
@@ -0,0 +1,10 @@
+# import frappe
+import unittest
+# from erpnext.e_commerce.product_data_engine.query import ProductQuery
+# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+
+test_dependencies = ["Item"]
+
+class TestVariantSelector(unittest.TestCase):
+ # TODO: Variant Selector Tests
+ pass
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_configurator/utils.py b/erpnext/e_commerce/variant_selector/utils.py
similarity index 98%
rename from erpnext/e_commerce/product_configurator/utils.py
rename to erpnext/e_commerce/variant_selector/utils.py
index 5ea32f9..2e1852c 100644
--- a/erpnext/e_commerce/product_configurator/utils.py
+++ b/erpnext/e_commerce/variant_selector/utils.py
@@ -1,7 +1,6 @@
import frappe
from frappe.utils import cint
-
-from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager
+from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
items = []
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index c719d50..bfca3f4 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -502,12 +502,7 @@
}
.item-configurator-dialog {
- .modal-header {
- padding: var(--padding-md) var(--padding-xl);
- }
-
.modal-body {
- padding: 0 var(--padding-xl);
padding-bottom: var(--padding-xl);
.status-area {
@@ -1292,13 +1287,10 @@
font-size: 72px;
}
-.modal-backdrop {
- position: fixed;
- top: 0;
- right: 0;
- left: 0;
- background-color: var(--gray-100);
- height: 100%;
+[data-path="cart"] {
+ .modal-backdrop {
+ background-color: var(--gray-50); // lighter backdrop only on cart freeze
+ }
}
.item-thumb {
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index f4b667e..3d1a1d0 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -104,16 +104,23 @@
def delete_child_item_groups_key(self):
frappe.cache().hdel("child_item_groups", self.name)
- def validate_item_group_defaults(self):
- from erpnext.stock.doctype.item.item import validate_item_default_company_links
- validate_item_default_company_links(self.item_group_defaults)
-
-def get_child_groups(item_group_name):
+def get_child_groups_for_website(item_group_name, immediate=False):
"""Returns child item groups *excluding* passed group."""
- item_group = frappe.get_doc("Item Group", item_group_name)
- return frappe.db.sql("""select name, route
- from `tabItem Group` where lft>%(lft)s and rgt<%(rgt)s
- and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt}, as_dict=1)
+ item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
+ filters = {
+ "lft": [">", item_group.lft],
+ "rgt": ["<", item_group.rgt],
+ "show_in_website": 1
+ }
+
+ if immediate:
+ filters["parent_item_group"] = item_group_name
+
+ return frappe.get_all(
+ "Item Group",
+ filters=filters,
+ fields=["name", "route"]
+ )
def get_child_item_groups(item_group_name):
item_group = frappe.get_cached_value("Item Group",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index de023ca..52e3b40 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -914,7 +914,7 @@
def invalidate_item_variants_cache_for_website(doc):
"""Rebuild ItemVariantsCacheManager via Item or Website Item."""
- from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager
+ from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
item_code = None
is_web_item = doc.get("published_in_website") or doc.get("published")
diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html
index 9ff1d79..fcab594 100644
--- a/erpnext/templates/generators/item/item_configure.html
+++ b/erpnext/templates/generators/item/item_configure.html
@@ -3,11 +3,11 @@
<div class="mt-5 mb-6">
{% if cart_settings.enable_variants | int %}
- <button class="btn btn-primary-light btn-configure font-md"
+ <button class="btn btn-primary-light btn-configure font-md mr-2"
data-item-code="{{ doc.name }}"
data-item-name="{{ doc.item_name }}"
>
- {{ _('Configure') }}
+ {{ _('Select Variant') }}
</button>
{% endif %}
{% if cart_settings.show_contact_us_button %}
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
index f47650a..3220226 100644
--- a/erpnext/templates/generators/item/item_configure.js
+++ b/erpnext/templates/generators/item/item_configure.js
@@ -29,7 +29,7 @@
});
this.dialog = new frappe.ui.Dialog({
- title: __('Configure {0}', [this.item_name]),
+ title: __('Select Variant for {0}', [this.item_name]),
fields,
on_hide: () => {
set_continue_configuration();
@@ -280,14 +280,14 @@
}
get_next_attribute_and_values(selected_attributes) {
- return this.call('erpnext.e_commerce.product_configurator.utils.get_next_attribute_and_values', {
+ return this.call('erpnext.e_commerce.variant_selector.utils.get_next_attribute_and_values', {
item_code: this.item_code,
selected_attributes
});
}
get_attributes_and_values() {
- return this.call('erpnext.e_commerce.product_configurator.utils.get_attributes_and_values', {
+ return this.call('erpnext.e_commerce.variant_selector.utils.get_attributes_and_values', {
item_code: this.item_code
});
}
@@ -311,9 +311,9 @@
const { itemCode } = $btn_configure.data();
if (localStorage.getItem(`configure:${itemCode}`)) {
- $btn_configure.text(__('Continue Configuration'));
+ $btn_configure.text(__('Continue Selection'));
} else {
- $btn_configure.text(__('Configure'));
+ $btn_configure.text(__('Select Variant'));
}
}