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
This commit is contained in:
marination 2021-08-17 00:48:36 +05:30
parent 7d1df9d4c3
commit 80fbe16be8
16 changed files with 574 additions and 198 deletions

View File

@ -132,7 +132,7 @@ def find_variant(template, args, variant_item_code=None):
conditions = " or ".join(conditions) 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] possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code]
for variant in possible_variants: for variant in possible_variants:

View File

@ -8,11 +8,22 @@ from frappe.utils import cint
from erpnext.e_commerce.product_data_engine.query import ProductQuery from erpnext.e_commerce.product_data_engine.query import ProductQuery
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder 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) @frappe.whitelist(allow_guest=True)
def get_product_filter_data(query_args=None): 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): if isinstance(query_args, str):
query_args = json.loads(query_args) query_args = json.loads(query_args)
@ -35,7 +46,7 @@ def get_product_filter_data(query_args=None):
sub_categories = [] sub_categories = []
if item_group: if item_group:
field_filters['item_group'] = 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() engine = ProductQuery()
try: try:
@ -46,7 +57,7 @@ def get_product_filter_data(query_args=None):
start=start, start=start,
item_group=item_group item_group=item_group
) )
except Exception as e: except Exception:
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(traceback, frappe._("Product Engine Error")) frappe.log_error(traceback, frappe._("Product Engine Error"))
return {"exc": "Something went wrong!"} return {"exc": "Something went wrong!"}

View File

@ -26,6 +26,10 @@ class TestWebsiteItem(unittest.TestCase):
"price_list": "_Test Price List India" "price_list": "_Test Price List India"
}) })
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
def setUp(self): def setUp(self):
if self._testMethodName in WEBITEM_DESK_TESTS: if self._testMethodName in WEBITEM_DESK_TESTS:
make_item("Test Web Item", { make_item("Test Web Item", {
@ -38,22 +42,13 @@ class TestWebsiteItem(unittest.TestCase):
] ]
}) })
elif self._testMethodName in WEBITEM_PRICE_TESTS: 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_item_price(item_code="Test Mobile Phone")
make_web_pricing_rule( make_web_pricing_rule(
title="Test Pricing Rule for Test Mobile Phone", title="Test Pricing Rule for Test Mobile Phone",
item_code="Test Mobile Phone", item_code="Test Mobile Phone",
selling=1) 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): def test_index_creation(self):
"Check if index is getting created in db." "Check if index is getting created in db."
from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update
@ -105,6 +100,8 @@ class TestWebsiteItem(unittest.TestCase):
item.reload() item.reload()
self.assertEqual(item.published_in_website, 0) self.assertEqual(item.published_in_website, 0)
item.delete()
def test_publish_variant_and_template(self): def test_publish_variant_and_template(self):
"Check if template is published on publishing variant." "Check if template is published on publishing variant."
# template "Test Web Item" created on setUp # template "Test Web Item" created on setUp
@ -256,7 +253,7 @@ class TestWebsiteItem(unittest.TestCase):
2) Showing stock availability disabled 2) Showing stock availability disabled
""" """
item_code = "Test Mobile Phone" item_code = "Test Mobile Phone"
self.create_regular_web_item() create_regular_web_item()
setup_e_commerce_settings({"show_stock_availability": 1}) setup_e_commerce_settings({"show_stock_availability": 1})
frappe.local.shopping_cart_settings = None frappe.local.shopping_cart_settings = None
@ -298,7 +295,7 @@ class TestWebsiteItem(unittest.TestCase):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item_code = "Test Mobile Phone" item_code = "Test Mobile Phone"
self.create_regular_web_item() create_regular_web_item()
setup_e_commerce_settings({"show_stock_availability": 1}) setup_e_commerce_settings({"show_stock_availability": 1})
frappe.local.shopping_cart_settings = None frappe.local.shopping_cart_settings = None
@ -339,7 +336,7 @@ class TestWebsiteItem(unittest.TestCase):
def test_recommended_item(self): def test_recommended_item(self):
"Check if added recommended items are fetched correctly." "Check if added recommended items are fetched correctly."
item_code = "Test Mobile Phone" 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({ setup_e_commerce_settings({
"enable_recommendations": 1, "enable_recommendations": 1,
@ -347,7 +344,7 @@ class TestWebsiteItem(unittest.TestCase):
}) })
# create recommended web item and price for it # 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") make_web_item_price(item_code="Test Mobile Phone 1")
# add recommended item to first web item # add recommended item to first web item
@ -379,14 +376,14 @@ class TestWebsiteItem(unittest.TestCase):
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched
# tear down # tear down
frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone 1"}).delete()
web_item.delete() web_item.delete()
recommended_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): def test_recommended_item_for_guest_user(self):
"Check if added recommended items are fetched correctly for guest user." "Check if added recommended items are fetched correctly for guest user."
item_code = "Test Mobile Phone" 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 # price visible to guests
setup_e_commerce_settings({ setup_e_commerce_settings({
@ -396,7 +393,7 @@ class TestWebsiteItem(unittest.TestCase):
}) })
# create recommended web item and price for it # 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") make_web_item_price(item_code="Test Mobile Phone 1")
# add recommended item to first web item # add recommended item to first web item
@ -428,17 +425,19 @@ class TestWebsiteItem(unittest.TestCase):
# tear down # tear down
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone 1"}).delete()
web_item.delete() web_item.delete()
recommended_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): def create_regular_web_item(item_code=None, item_args=None, web_args=None):
"Create Regular Item and Website Item." "Create Regular Item and Website Item."
item_code = item_code or "Test Mobile Phone" item_code = item_code or "Test Mobile Phone"
item = make_item(item_code) item = make_item(item_code, properties=item_args)
if not frappe.db.exists("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) web_item = make_website_item(item, save=False)
if web_args:
web_item.update(web_args)
web_item.save() web_item.save()
else: else:
web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code}) web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})

View File

@ -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()

View File

@ -6,7 +6,7 @@ from frappe.utils import floor
class ProductFiltersBuilder: class ProductFiltersBuilder:
def __init__(self, item_group=None): 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") self.doc = frappe.get_doc("E Commerce Settings")
else: else:
self.doc = frappe.get_doc("Item Group", item_group) self.doc = frappe.get_doc("Item Group", item_group)
@ -17,36 +17,39 @@ class ProductFiltersBuilder:
if not self.item_group and not self.doc.enable_field_filters: if not self.item_group and not self.doc.enable_field_filters:
return 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') # filter valid field filters i.e. those that exist in Item
fields = [df for df in meta.fields if df.fieldname in filter_fields] 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: 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 df.fieldtype == "Link":
if self.item_group: if self.item_group:
or_filters.extend([ item_or_filters.extend([
["item_group", "=", self.item_group], ["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: else:
doctype = df.get_link_doctype() # table multiselect
values = list(link_doctype_values)
# 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)]
# Remove None # Remove None
if None in values: if None in values:
@ -57,6 +60,36 @@ class ProductFiltersBuilder:
return filter_data 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): def get_attribute_filters(self):
if not self.item_group and not self.doc.enable_attribute_filters: if not self.item_group and not self.doc.enable_attribute_filters:
return return
@ -92,9 +125,9 @@ class ProductFiltersBuilder:
def get_discount_filters(self, discounts): def get_discount_filters(self, discounts):
discount_filters = [] discount_filters = []
# [25.89, 60.5] # [25.89, 60.5] min max
min_discount, max_discount = discounts[0], discounts[1] 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_absolute, max_range_absolute = floor(min_discount), floor(max_discount)
min_range = int(min_discount - (min_range_absolute % 10)) # 20 min_range = int(min_discount - (min_range_absolute % 10)) # 20
max_range = int(max_discount - (max_range_absolute % 10)) # 60 max_range = int(max_discount - (max_range_absolute % 10)) # 60

View File

@ -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)

View File

@ -2,37 +2,337 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe 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): class TestProductDataEngine(unittest.TestCase):
"Test Products Querying for Product Listing." "Test Products Querying and Filters for Product Listing."
def test_product_list_ordering(self):
"Check if website items appear by ranking."
pass
def test_product_list_paging(self): @classmethod
pass 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): 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): 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): attribute_filters = {"Test Size": ["Large"]}
pass 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): # check if only items with Test Size 'Large' are fetched
pass 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): def test_product_list_discount_filter_builder(self):
pass "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): item_code = "Test 12I Laptop"
"Check if product is visible on multiple item group pages barring its own." make_web_item_price(item_code=item_code)
pass 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): 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)

View File

@ -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

View File

@ -1,7 +1,6 @@
import frappe import frappe
from frappe.utils import cint from frappe.utils import cint
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager
def get_item_codes_by_attributes(attribute_filters, template_item_code=None): def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
items = [] items = []

View File

@ -502,12 +502,7 @@ body.product-page {
} }
.item-configurator-dialog { .item-configurator-dialog {
.modal-header {
padding: var(--padding-md) var(--padding-xl);
}
.modal-body { .modal-body {
padding: 0 var(--padding-xl);
padding-bottom: var(--padding-xl); padding-bottom: var(--padding-xl);
.status-area { .status-area {
@ -1292,13 +1287,10 @@ body.product-page {
font-size: 72px; font-size: 72px;
} }
.modal-backdrop { [data-path="cart"] {
position: fixed; .modal-backdrop {
top: 0; background-color: var(--gray-50); // lighter backdrop only on cart freeze
right: 0; }
left: 0;
background-color: var(--gray-100);
height: 100%;
} }
.item-thumb { .item-thumb {

View File

@ -104,16 +104,23 @@ class ItemGroup(NestedSet, WebsiteGenerator):
def delete_child_item_groups_key(self): def delete_child_item_groups_key(self):
frappe.cache().hdel("child_item_groups", self.name) frappe.cache().hdel("child_item_groups", self.name)
def validate_item_group_defaults(self): def get_child_groups_for_website(item_group_name, immediate=False):
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):
"""Returns child item groups *excluding* passed group.""" """Returns child item groups *excluding* passed group."""
item_group = frappe.get_doc("Item Group", item_group_name) item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
return frappe.db.sql("""select name, route filters = {
from `tabItem Group` where lft>%(lft)s and rgt<%(rgt)s "lft": [">", item_group.lft],
and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt}, as_dict=1) "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): def get_child_item_groups(item_group_name):
item_group = frappe.get_cached_value("Item Group", item_group = frappe.get_cached_value("Item Group",

View File

@ -914,7 +914,7 @@ def invalidate_cache_for_item(doc):
def invalidate_item_variants_cache_for_website(doc): def invalidate_item_variants_cache_for_website(doc):
"""Rebuild ItemVariantsCacheManager via Item or Website Item.""" """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 item_code = None
is_web_item = doc.get("published_in_website") or doc.get("published") is_web_item = doc.get("published_in_website") or doc.get("published")

View File

@ -3,11 +3,11 @@
<div class="mt-5 mb-6"> <div class="mt-5 mb-6">
{% if cart_settings.enable_variants | int %} {% 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-code="{{ doc.name }}"
data-item-name="{{ doc.item_name }}" data-item-name="{{ doc.item_name }}"
> >
{{ _('Configure') }} {{ _('Select Variant') }}
</button> </button>
{% endif %} {% endif %}
{% if cart_settings.show_contact_us_button %} {% if cart_settings.show_contact_us_button %}

View File

@ -29,7 +29,7 @@ class ItemConfigure {
}); });
this.dialog = new frappe.ui.Dialog({ this.dialog = new frappe.ui.Dialog({
title: __('Configure {0}', [this.item_name]), title: __('Select Variant for {0}', [this.item_name]),
fields, fields,
on_hide: () => { on_hide: () => {
set_continue_configuration(); set_continue_configuration();
@ -280,14 +280,14 @@ class ItemConfigure {
} }
get_next_attribute_and_values(selected_attributes) { 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, item_code: this.item_code,
selected_attributes selected_attributes
}); });
} }
get_attributes_and_values() { 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 item_code: this.item_code
}); });
} }
@ -311,9 +311,9 @@ function set_continue_configuration() {
const { itemCode } = $btn_configure.data(); const { itemCode } = $btn_configure.data();
if (localStorage.getItem(`configure:${itemCode}`)) { if (localStorage.getItem(`configure:${itemCode}`)) {
$btn_configure.text(__('Continue Configuration')); $btn_configure.text(__('Continue Selection'));
} else { } else {
$btn_configure.text(__('Configure')); $btn_configure.text(__('Select Variant'));
} }
} }