style: format code with black

This commit is contained in:
Ankush Menat
2022-03-28 18:52:46 +05:30
parent 21e00da3d6
commit 494bd9ef78
1395 changed files with 91704 additions and 62532 deletions

View File

@@ -15,16 +15,16 @@ from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_web
@frappe.whitelist(allow_guest=True)
def get_product_filter_data(query_args=None):
"""
Returns filtered products and discount filters.
:param query_args (dict): contains filters to get products list
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
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)
@@ -53,11 +53,7 @@ def get_product_filter_data(query_args=None):
engine = ProductQuery()
try:
result = engine.query(
attribute_filters,
field_filters,
search_term=search,
start=start,
item_group=item_group
attribute_filters, field_filters, search_term=search, start=start, item_group=item_group
)
except Exception:
traceback = frappe.get_traceback()
@@ -77,9 +73,10 @@ def get_product_filter_data(query_args=None):
"filters": filters,
"settings": engine.settings,
"sub_categories": sub_categories,
"items_count": result["items_count"]
"items_count": result["items_count"],
}
@frappe.whitelist(allow_guest=True)
def get_guest_redirect_on_action():
return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action")
return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action")

View File

@@ -14,7 +14,9 @@ from erpnext.e_commerce.redisearch_utils import (
)
class ShoppingCartSetupError(frappe.ValidationError): pass
class ShoppingCartSetupError(frappe.ValidationError):
pass
class ECommerceSettings(Document):
def onload(self):
@@ -37,11 +39,17 @@ class ECommerceSettings(Document):
return
item_meta = frappe.get_meta("Item")
valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]]
valid_fields = [
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
]
for f in self.filter_fields:
if f.fieldname not in valid_fields:
frappe.throw(_("Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'").format(f.idx, f.fieldname))
frappe.throw(
_(
"Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'"
).format(f.idx, f.fieldname)
)
def validate_attribute_filters(self):
if not (self.enable_attribute_filters and self.filter_attributes):
@@ -58,8 +66,8 @@ class ECommerceSettings(Document):
if not self.search_index_fields:
return
fields = self.search_index_fields.replace(' ', '')
fields = unique(fields.strip(',').split(',')) # Remove extra ',' and remove duplicates
fields = self.search_index_fields.replace(" ", "")
fields = unique(fields.strip(",").split(",")) # Remove extra ',' and remove duplicates
# All fields should be indexable
allowed_indexable_fields = get_indexable_web_fields()
@@ -70,18 +78,22 @@ class ECommerceSettings(Document):
invalid_fields = comma_and(invalid_fields)
if num_invalid_fields > 1:
frappe.throw(_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields)))
frappe.throw(
_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields))
)
else:
frappe.throw(_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields)))
frappe.throw(
_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields))
)
self.search_index_fields = ','.join(fields)
self.search_index_fields = ",".join(fields)
def validate_price_list_exchange_rate(self):
"Check if exchange rate exists for Price List currency (to Company's currency)."
from erpnext.setup.utils import get_exchange_rate
if not self.enabled or not self.company or not self.price_list:
return # this function is also called from hooks, check values again
return # this function is also called from hooks, check values again
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency")
@@ -105,12 +117,13 @@ class ECommerceSettings(Document):
frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError)
def validate_tax_rule(self):
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"):
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError)
def get_tax_master(self, billing_territory):
tax_master = self.get_name_from_territory(billing_territory, "sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master")
tax_master = self.get_name_from_territory(
billing_territory, "sales_taxes_and_charges_masters", "sales_taxes_and_charges_master"
)
return tax_master and tax_master[0] or None
def get_shipping_rules(self, shipping_territory):
@@ -127,25 +140,33 @@ class ECommerceSettings(Document):
if not (new_fields == old_fields):
create_website_items_index()
def validate_cart_settings(doc=None, method=None):
frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
def get_shopping_cart_settings():
if not getattr(frappe.local, "shopping_cart_settings", None):
frappe.local.shopping_cart_settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
frappe.local.shopping_cart_settings = frappe.get_doc(
"E Commerce Settings", "E Commerce Settings"
)
return frappe.local.shopping_cart_settings
@frappe.whitelist(allow_guest=True)
def is_cart_enabled():
return get_shopping_cart_settings().enabled
def show_quantity_in_website():
return get_shopping_cart_settings().show_quantity_in_website
def check_shopping_cart_enabled():
if not get_shopping_cart_settings().enabled:
frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError)
def show_attachments():
return get_shopping_cart_settings().show_attachments

View File

@@ -15,8 +15,7 @@ class TestECommerceSettings(unittest.TestCase):
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
def get_cart_settings(self):
return frappe.get_doc({"doctype": "E Commerce Settings",
"company": "_Test Company"})
return frappe.get_doc({"doctype": "E Commerce Settings", "company": "_Test Company"})
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
# We aren't checking just currency exchange record anymore
@@ -41,7 +40,7 @@ class TestECommerceSettings(unittest.TestCase):
def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
frappe.db.commit() # nosemgrep
frappe.db.commit() # nosemgrep
cart_settings = self.get_cart_settings()
cart_settings.enabled = 1
@@ -50,6 +49,7 @@ class TestECommerceSettings(unittest.TestCase):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
def setup_e_commerce_settings(values_dict):
"Accepts a dict of values that updates E Commerce Settings."
if not values_dict:
@@ -59,4 +59,5 @@ def setup_e_commerce_settings(values_dict):
doc.update(values_dict)
doc.save()
test_dependencies = ["Tax Rule"]

View File

@@ -18,6 +18,7 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
class UnverifiedReviewer(frappe.ValidationError):
pass
class ItemReview(Document):
def after_insert(self):
# regenerate cache on review creation
@@ -54,12 +55,13 @@ def get_item_reviews(web_item, start=0, end=10, data=None):
return data
def get_queried_reviews(web_item, start=0, end=10, data=None):
"""
Query Website Item wise reviews and cache if needed.
Cache stores only first page of reviews i.e. 10 reviews maximum.
Returns:
dict: Containing reviews, average ratings, % of reviews per rating and total reviews.
Query Website Item wise reviews and cache if needed.
Cache stores only first page of reviews i.e. 10 reviews maximum.
Returns:
dict: Containing reviews, average ratings, % of reviews per rating and total reviews.
"""
if not data:
data = frappe._dict()
@@ -69,13 +71,13 @@ def get_queried_reviews(web_item, start=0, end=10, data=None):
filters={"website_item": web_item},
fields=["*"],
limit_start=start,
limit_page_length=end
limit_page_length=end,
)
rating_data = frappe.db.get_all(
"Item Review",
filters={"website_item": web_item},
fields=["avg(rating) as average, count(*) as total"]
fields=["avg(rating) as average, count(*) as total"],
)[0]
data.average_rating = flt(rating_data.average, 1)
@@ -83,11 +85,9 @@ def get_queried_reviews(web_item, start=0, end=10, data=None):
# get % of reviews per rating
reviews_per_rating = []
for i in range(1,6):
for i in range(1, 6):
count = frappe.db.get_all(
"Item Review",
filters={"website_item": web_item, "rating": i},
fields=["count(*) as count"]
"Item Review", filters={"website_item": web_item, "rating": i}, fields=["count(*) as count"]
)[0].count
percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0
@@ -98,40 +98,45 @@ def get_queried_reviews(web_item, start=0, end=10, data=None):
return data
def set_reviews_in_cache(web_item, reviews_dict):
frappe.cache().hset("item_reviews", web_item, reviews_dict)
@frappe.whitelist()
def add_item_review(web_item, title, rating, comment=None):
""" Add an Item Review by a user if non-existent. """
"""Add an Item Review by a user if non-existent."""
if frappe.session.user == "Guest":
# guest user should not reach here ideally in the case they do via an API, throw error
frappe.throw(_("You are not verified to write a review yet."), exc=UnverifiedReviewer)
if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}):
doc = frappe.get_doc({
"doctype": "Item Review",
"user": frappe.session.user,
"customer": get_customer(),
"website_item": web_item,
"item": frappe.db.get_value("Website Item", web_item, "item_code"),
"review_title": title,
"rating": rating,
"comment": comment
})
doc = frappe.get_doc(
{
"doctype": "Item Review",
"user": frappe.session.user,
"customer": get_customer(),
"website_item": web_item,
"item": frappe.db.get_value("Website Item", web_item, "item_code"),
"review_title": title,
"rating": rating,
"comment": comment,
}
)
doc.published_on = datetime.today().strftime("%d %B %Y")
doc.insert()
def get_customer(silent=False):
"""
silent: Return customer if exists else return nothing. Dont throw error.
silent: Return customer if exists else return nothing. Dont throw error.
"""
user = frappe.session.user
contact_name = get_contact_name(user)
customer = None
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
contact = frappe.get_doc("Contact", contact_name)
for link in contact.links:
if link.link_doctype == "Customer":
customer = link.link_name
@@ -143,5 +148,6 @@ def get_customer(silent=False):
return None
else:
# should not reach here unless via an API
frappe.throw(_("You are not a verified customer yet. Please contact us to proceed."),
exc=UnverifiedReviewer)
frappe.throw(
_("You are not a verified customer yet. Please contact us to proceed."), exc=UnverifiedReviewer
)

View File

@@ -19,17 +19,23 @@ from erpnext.stock.doctype.item.item import DataValidationError
from erpnext.stock.doctype.item.test_item import make_item
WEBITEM_DESK_TESTS = ("test_website_item_desk_item_sync", "test_publish_variant_and_template")
WEBITEM_PRICE_TESTS = ('test_website_item_price_for_logged_in_user', 'test_website_item_price_for_guest_user')
WEBITEM_PRICE_TESTS = (
"test_website_item_price_for_logged_in_user",
"test_website_item_price_for_guest_user",
)
class TestWebsiteItem(unittest.TestCase):
@classmethod
def setUpClass(cls):
setup_e_commerce_settings({
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India"
})
setup_e_commerce_settings(
{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
}
)
@classmethod
def tearDownClass(cls):
@@ -37,40 +43,42 @@ class TestWebsiteItem(unittest.TestCase):
def setUp(self):
if self._testMethodName in WEBITEM_DESK_TESTS:
make_item("Test Web Item", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{
"attribute": "Test Size"
}
]
})
make_item(
"Test Web Item",
{
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [{"attribute": "Test Size"}],
},
)
elif self._testMethodName in WEBITEM_PRICE_TESTS:
create_user_and_customer_if_not_exists("test_contact_customer@example.com", "_Test Contact For _Test Customer")
create_user_and_customer_if_not_exists(
"test_contact_customer@example.com", "_Test Contact For _Test Customer"
)
create_regular_web_item()
make_web_item_price(item_code="Test Mobile Phone")
# Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass.
# This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor,
# when testing for logged-in user the test will get the previous pricing rule because "selling" is still true.
# This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor,
# when testing for logged-in user the test will get the previous pricing rule because "selling" is still true.
#
# I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test.
make_web_pricing_rule(
title="Test Pricing Rule for Test Mobile Phone",
item_code="Test Mobile Phone",
selling=1)
title="Test Pricing Rule for Test Mobile Phone", item_code="Test Mobile Phone", selling=1
)
make_web_pricing_rule(
title="Test Pricing Rule for Test Mobile Phone (Customer)",
item_code="Test Mobile Phone",
selling=1,
discount_percentage="25",
applicable_for="Customer",
customer="_Test Customer")
customer="_Test Customer",
)
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
on_doctype_update()
indices = frappe.db.sql("show index from `tabWebsite Item`", as_dict=1)
@@ -84,7 +92,7 @@ class TestWebsiteItem(unittest.TestCase):
def test_website_item_desk_item_sync(self):
"Check creation/updation/deletion of Website Item and its impact on Item master."
web_item = None
item = make_item("Test Web Item") # will return item if exists
item = make_item("Test Web Item") # will return item if exists
try:
web_item = make_website_item(item, save=False)
web_item.save()
@@ -97,7 +105,7 @@ class TestWebsiteItem(unittest.TestCase):
item.reload()
self.assertEqual(web_item.published, 1)
self.assertEqual(item.published_in_website, 1) # check if item was back updated
self.assertEqual(item.published_in_website, 1) # check if item was back updated
self.assertEqual(web_item.item_group, item.item_group)
# check if changing item data changes it in website item
@@ -170,9 +178,12 @@ class TestWebsiteItem(unittest.TestCase):
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
item_code = "Test Breadcrumb Item"
item = make_item(item_code, {
"item_group": "_Test Item Group B - 1",
})
item = make_item(
item_code,
{
"item_group": "_Test Item Group B - 1",
},
)
if not frappe.db.exists("Website Item", {"item_code": item_code}):
web_item = make_website_item(item, save=False)
@@ -187,7 +198,7 @@ class TestWebsiteItem(unittest.TestCase):
self.assertEqual(breadcrumbs[0]["name"], "Home")
self.assertEqual(breadcrumbs[1]["name"], "Shop by Category")
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
# tear down
@@ -236,10 +247,7 @@ class TestWebsiteItem(unittest.TestCase):
item_code = "Test Mobile Phone"
# show price for guest user in e commerce settings
setup_e_commerce_settings({
"show_price": 1,
"hide_price_for_guest": 0
})
setup_e_commerce_settings({"show_price": 1, "hide_price_for_guest": 0})
# price and pricing rule added via setUp
@@ -270,11 +278,11 @@ class TestWebsiteItem(unittest.TestCase):
def test_website_item_stock_when_out_of_stock(self):
"""
Check if stock details are fetched correctly for empty inventory when:
1) Showing stock availability enabled:
- Warehouse unset
- Warehouse set
2) Showing stock availability disabled
Check if stock details are fetched correctly for empty inventory when:
1) Showing stock availability enabled:
- Warehouse unset
- Warehouse set
2) Showing stock availability disabled
"""
item_code = "Test Mobile Phone"
create_regular_web_item()
@@ -288,7 +296,9 @@ class TestWebsiteItem(unittest.TestCase):
self.assertFalse(bool(data.product_info["stock_qty"]))
# set warehouse
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC")
frappe.db.set_value(
"Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC"
)
# check if stock details are fetched and item not in stock with warehouse set
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
@@ -310,11 +320,11 @@ class TestWebsiteItem(unittest.TestCase):
def test_website_item_stock_when_in_stock(self):
"""
Check if stock details are fetched correctly for available inventory when:
1) Showing stock availability enabled:
- Warehouse set
- Warehouse unset
2) Showing stock availability disabled
Check if stock details are fetched correctly for available inventory when:
1) Showing stock availability enabled:
- Warehouse set
- Warehouse unset
2) Showing stock availability disabled
"""
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -324,10 +334,14 @@ class TestWebsiteItem(unittest.TestCase):
frappe.local.shopping_cart_settings = None
# set warehouse
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC")
frappe.db.set_value(
"Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC"
)
# stock up item
stock_entry = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100)
stock_entry = make_stock_entry(
item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100
)
# check if stock details are fetched and item is in stock with warehouse set
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
@@ -362,10 +376,7 @@ class TestWebsiteItem(unittest.TestCase):
item_code = "Test Mobile Phone"
web_item = create_regular_web_item(item_code)
setup_e_commerce_settings({
"enable_recommendations": 1,
"show_price": 1
})
setup_e_commerce_settings({"enable_recommendations": 1, "show_price": 1})
# create recommended web item and price for it
recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
@@ -383,7 +394,7 @@ class TestWebsiteItem(unittest.TestCase):
self.assertEqual(len(recommended_items), 1)
recomm_item = recommended_items[0]
self.assertEqual(recomm_item.get("website_item_name"), "Test Mobile Phone 1")
self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched
self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched
price_info = recomm_item.get("price_info")
self.assertEqual(price_info.get("price_list_rate"), 1000)
@@ -397,7 +408,7 @@ class TestWebsiteItem(unittest.TestCase):
recommended_items = web_item.get_recommended_items(e_commerce_settings)
self.assertEqual(len(recommended_items), 1)
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
web_item.delete()
@@ -410,11 +421,9 @@ class TestWebsiteItem(unittest.TestCase):
web_item = create_regular_web_item(item_code)
# price visible to guests
setup_e_commerce_settings({
"enable_recommendations": 1,
"show_price": 1,
"hide_price_for_guest": 0
})
setup_e_commerce_settings(
{"enable_recommendations": 1, "show_price": 1, "hide_price_for_guest": 0}
)
# create recommended web item and price for it
recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
@@ -432,7 +441,7 @@ class TestWebsiteItem(unittest.TestCase):
# test results if show price is enabled
self.assertEqual(len(recommended_items), 1)
self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched
self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched
# price hidden from guests
frappe.set_user("Administrator")
@@ -445,7 +454,7 @@ class TestWebsiteItem(unittest.TestCase):
# test results if show price is enabled
self.assertEqual(len(recommended_items), 1)
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched
# tear down
frappe.set_user("Administrator")
@@ -453,6 +462,7 @@ class TestWebsiteItem(unittest.TestCase):
recommended_web_item.delete()
frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
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"
@@ -468,47 +478,51 @@ def create_regular_web_item(item_code=None, item_args=None, web_args=None):
return web_item
def make_web_item_price(**kwargs):
item_code = kwargs.get("item_code")
if not item_code:
return
if not frappe.db.exists("Item Price", {"item_code": item_code}):
item_price = frappe.get_doc({
"doctype": "Item Price",
"item_code": item_code,
"price_list": kwargs.get("price_list") or "_Test Price List India",
"price_list_rate": kwargs.get("price_list_rate") or 1000
})
item_price = frappe.get_doc(
{
"doctype": "Item Price",
"item_code": item_code,
"price_list": kwargs.get("price_list") or "_Test Price List India",
"price_list_rate": kwargs.get("price_list_rate") or 1000,
}
)
item_price.insert()
else:
item_price = frappe.get_cached_doc("Item Price", {"item_code": item_code})
return item_price
def make_web_pricing_rule(**kwargs):
title = kwargs.get("title")
if not title:
return
if not frappe.db.exists("Pricing Rule", title):
pricing_rule = frappe.get_doc({
"doctype": "Pricing Rule",
"title": title,
"apply_on": kwargs.get("apply_on") or "Item Code",
"items": [{
"item_code": kwargs.get("item_code")
}],
"selling": kwargs.get("selling") or 0,
"buying": kwargs.get("buying") or 0,
"rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage",
"discount_percentage": kwargs.get("discount_percentage") or 10,
"company": kwargs.get("company") or "_Test Company",
"currency": kwargs.get("currency") or "INR",
"for_price_list": kwargs.get("price_list") or "_Test Price List India",
"applicable_for": kwargs.get("applicable_for") or "",
"customer": kwargs.get("customer") or "",
})
pricing_rule = frappe.get_doc(
{
"doctype": "Pricing Rule",
"title": title,
"apply_on": kwargs.get("apply_on") or "Item Code",
"items": [{"item_code": kwargs.get("item_code")}],
"selling": kwargs.get("selling") or 0,
"buying": kwargs.get("buying") or 0,
"rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage",
"discount_percentage": kwargs.get("discount_percentage") or 10,
"company": kwargs.get("company") or "_Test Company",
"currency": kwargs.get("currency") or "INR",
"for_price_list": kwargs.get("price_list") or "_Test Price List India",
"applicable_for": kwargs.get("applicable_for") or "",
"customer": kwargs.get("customer") or "",
}
)
pricing_rule.insert()
else:
pricing_rule = frappe.get_doc("Pricing Rule", {"title": title})
@@ -516,23 +530,26 @@ def make_web_pricing_rule(**kwargs):
return pricing_rule
def create_user_and_customer_if_not_exists(email, first_name = None):
def create_user_and_customer_if_not_exists(email, first_name=None):
if frappe.db.exists("User", email):
return
frappe.get_doc({
"doctype": "User",
"user_type": "Website User",
"email": email,
"send_welcome_email": 0,
"first_name": first_name or email.split("@")[0]
}).insert(ignore_permissions=True)
frappe.get_doc(
{
"doctype": "User",
"user_type": "Website User",
"email": email,
"send_welcome_email": 0,
"first_name": first_name or email.split("@")[0],
}
).insert(ignore_permissions=True)
contact = frappe.get_last_doc("Contact", filters={"email_id": email})
link = contact.append('links', {})
link = contact.append("links", {})
link.link_doctype = "Customer"
link.link_name = "_Test Customer"
link.link_title = "_Test Customer"
contact.save()
test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"]

View File

@@ -29,7 +29,7 @@ class WebsiteItem(WebsiteGenerator):
page_title_field="web_item_name",
condition_field="published",
template="templates/generators/item/item.html",
no_cache=1
no_cache=1,
)
def autoname(self):
@@ -88,14 +88,17 @@ class WebsiteItem(WebsiteGenerator):
def publish_unpublish_desk_item(self, publish=True):
if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish:
return # if already published don't publish again
return # if already published don't publish again
frappe.db.set_value("Item", self.item_code, "published_in_website", publish)
def make_route(self):
"""Called from set_route in WebsiteGenerator."""
if not self.route:
return cstr(frappe.db.get_value('Item Group', self.item_group,
'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5))
return (
cstr(frappe.db.get_value("Item Group", self.item_group, "route"))
+ "/"
+ self.scrub((self.item_name if self.item_name else self.item_code) + "-" + random_string(5))
)
def update_template_item(self):
"""Publish Template Item if Variant is published."""
@@ -124,12 +127,10 @@ class WebsiteItem(WebsiteGenerator):
# find if website image url exists as public
file_doc = frappe.get_all(
"File",
filters={
"file_url": self.website_image
},
filters={"file_url": self.website_image},
fields=["name", "is_private"],
order_by="is_private asc",
limit_page_length=1
limit_page_length=1,
)
if file_doc:
@@ -137,7 +138,11 @@ class WebsiteItem(WebsiteGenerator):
if not file_doc:
if not auto_set_website_image:
frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name))
frappe.msgprint(
_("Website Image {0} attached to Item {1} cannot be found").format(
self.website_image, self.name
)
)
self.website_image = None
@@ -154,18 +159,23 @@ class WebsiteItem(WebsiteGenerator):
import requests.exceptions
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
if not self.is_new() and self.website_image != frappe.db.get_value(
self.doctype, self.name, "website_image"
):
self.thumbnail = None
if self.website_image and not self.thumbnail:
file_doc = None
try:
file_doc = frappe.get_doc("File", {
"file_url": self.website_image,
"attached_to_doctype": "Website Item",
"attached_to_name": self.name
})
file_doc = frappe.get_doc(
"File",
{
"file_url": self.website_image,
"attached_to_doctype": "Website Item",
"attached_to_name": self.name,
},
)
except frappe.DoesNotExistError:
pass
# cleanup
@@ -177,18 +187,21 @@ class WebsiteItem(WebsiteGenerator):
except requests.exceptions.SSLError:
frappe.msgprint(
_("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image))
_("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)
)
self.website_image = None
# for CSV import
if self.website_image and not file_doc:
try:
file_doc = frappe.get_doc({
"doctype": "File",
"file_url": self.website_image,
"attached_to_doctype": "Website Item",
"attached_to_name": self.name
}).save()
file_doc = frappe.get_doc(
{
"doctype": "File",
"file_url": self.website_image,
"attached_to_doctype": "Website Item",
"attached_to_name": self.name,
}
).save()
except IOError:
self.website_image = None
@@ -204,11 +217,11 @@ class WebsiteItem(WebsiteGenerator):
context.search_link = "/search"
context.body_class = "product-page"
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
self.attributes = frappe.get_all(
"Item Variant Attribute",
fields=["attribute", "attribute_value"],
filters={"parent": self.item_code}
filters={"parent": self.item_code},
)
if self.slideshow:
@@ -227,7 +240,9 @@ class WebsiteItem(WebsiteGenerator):
context.reviews = context.reviews[:4]
context.wished = False
if frappe.db.exists("Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}):
if frappe.db.exists(
"Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}
):
context.wished = True
context.user_is_customer = check_if_user_is_customer()
@@ -243,11 +258,12 @@ class WebsiteItem(WebsiteGenerator):
variant.attributes = frappe.get_all(
"Item Variant Attribute",
filters={"parent": variant.name},
fields=["attribute", "attribute_value as value"])
fields=["attribute", "attribute_value as value"],
)
# make an attribute-value map for easier access in templates
variant.attribute_map = frappe._dict(
{attr.attribute : attr.value for attr in variant.attributes}
{attr.attribute: attr.value for attr in variant.attributes}
)
for attr in variant.attributes:
@@ -267,9 +283,12 @@ class WebsiteItem(WebsiteGenerator):
values.append(val)
else:
# get list of values defined (for sequence)
for attr_value in frappe.db.get_all("Item Attribute Value",
for attr_value in frappe.db.get_all(
"Item Attribute Value",
fields=["attribute_value"],
filters={"parent": attr.attribute}, order_by="idx asc"):
filters={"parent": attr.attribute},
order_by="idx asc",
):
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
values.append(attr_value.attribute_value)
@@ -279,10 +298,10 @@ class WebsiteItem(WebsiteGenerator):
safe_description = frappe.utils.to_markdown(self.description)
context.metatags.url = frappe.utils.get_url() + '/' + context.route
context.metatags.url = frappe.utils.get_url() + "/" + context.route
if context.website_image:
if context.website_image.startswith('http'):
if context.website_image.startswith("http"):
url = context.website_image
else:
url = frappe.utils.get_url() + context.website_image
@@ -292,24 +311,28 @@ class WebsiteItem(WebsiteGenerator):
context.metatags.title = self.web_item_name or self.item_name or self.item_code
context.metatags['og:type'] = 'product'
context.metatags['og:site_name'] = 'ERPNext'
context.metatags["og:type"] = "product"
context.metatags["og:site_name"] = "ERPNext"
def set_shopping_cart_data(self, context):
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
context.shopping_cart = get_product_info_for_website(self.item_code, skip_quotation_creation=True)
context.shopping_cart = get_product_info_for_website(
self.item_code, skip_quotation_creation=True
)
def copy_specification_from_item_group(self):
self.set("website_specifications", [])
if self.item_group:
for label, desc in frappe.db.get_values("Item Website Specification",
{"parent": self.item_group}, ["label", "description"]):
for label, desc in frappe.db.get_values(
"Item Website Specification", {"parent": self.item_group}, ["label", "description"]
):
row = self.append("website_specifications")
row.label = label
row.description = desc
def get_product_details_section(self, context):
""" Get section with tabs or website specifications. """
"""Get section with tabs or website specifications."""
context.show_tabs = self.show_tabbed_section
if self.show_tabbed_section and (self.tabs or self.website_specifications):
context.tabs = self.get_tabs()
@@ -321,10 +344,8 @@ class WebsiteItem(WebsiteGenerator):
tab_values["tab_1_title"] = "Product Details"
tab_values["tab_1_content"] = frappe.render_template(
"templates/generators/item/item_specifications.html",
{
"website_specifications": self.website_specifications,
"show_tabs": self.show_tabbed_section
})
{"website_specifications": self.website_specifications, "show_tabs": self.show_tabbed_section},
)
for row in self.tabs:
tab_values[f"tab_{row.idx + 1}_title"] = _(row.label)
@@ -338,15 +359,11 @@ class WebsiteItem(WebsiteGenerator):
query = (
frappe.qb.from_(ri)
.join(wi).on(ri.item_code == wi.item_code)
.select(
ri.item_code, ri.route,
ri.website_item_name,
ri.website_item_thumbnail
).where(
(ri.parent == self.name)
& (wi.published == 1)
).orderby(ri.idx)
.join(wi)
.on(ri.item_code == wi.item_code)
.select(ri.item_code, ri.route, ri.website_item_name, ri.website_item_thumbnail)
.where((ri.parent == self.name) & (wi.published == 1))
.orderby(ri.idx)
)
items = query.run(as_dict=True)
@@ -360,22 +377,24 @@ class WebsiteItem(WebsiteGenerator):
selling_price_list = _set_price_list(settings, None)
for item in items:
item.price_info = get_price(
item.item_code,
selling_price_list,
settings.default_customer_group,
settings.company
item.item_code, selling_price_list, settings.default_customer_group, settings.company
)
return items
def invalidate_cache_for_web_item(doc):
"""Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager."""
from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website
invalidate_cache_for(doc, doc.item_group)
website_item_groups = list(set((doc.get("old_website_item_groups") or [])
+ [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group]))
website_item_groups = list(
set(
(doc.get("old_website_item_groups") or [])
+ [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group]
)
)
for item_group in website_item_groups:
invalidate_cache_for(doc, item_group)
@@ -385,6 +404,7 @@ def invalidate_cache_for_web_item(doc):
invalidate_item_variants_cache_for_website(doc)
def on_doctype_update():
# since route is a Text column, it needs a length for indexing
frappe.db.add_index("Website Item", ["route(500)"])
@@ -392,6 +412,7 @@ def on_doctype_update():
frappe.db.add_index("Website Item", ["item_group"])
frappe.db.add_index("Website Item", ["brand"])
def check_if_user_is_customer(user=None):
from frappe.contacts.doctype.contact.contact import get_contact_name
@@ -402,7 +423,7 @@ def check_if_user_is_customer(user=None):
customer = None
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
contact = frappe.get_doc("Contact", contact_name)
for link in contact.links:
if link.link_doctype == "Customer":
customer = link.link_name
@@ -410,6 +431,7 @@ def check_if_user_is_customer(user=None):
return True if customer else False
@frappe.whitelist()
def make_website_item(doc, save=True):
if not doc:
@@ -425,8 +447,17 @@ def make_website_item(doc, save=True):
website_item = frappe.new_doc("Website Item")
website_item.web_item_name = doc.get("item_name")
fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",
"has_variants", "variant_of", "description"]
fields_to_map = [
"item_code",
"item_name",
"item_group",
"stock_uom",
"brand",
"image",
"has_variants",
"variant_of",
"description",
]
for field in fields_to_map:
website_item.update({field: doc.get(field)})
@@ -438,4 +469,4 @@ def make_website_item(doc, save=True):
# Add to search cache
insert_item_to_index(website_item)
return [website_item.name, website_item.web_item_name]
return [website_item.name, website_item.web_item_name]

View File

@@ -9,6 +9,7 @@ from frappe.model.document import Document
class WebsiteOffer(Document):
pass
@frappe.whitelist(allow_guest=True)
def get_offer_details(offer_id):
return frappe.db.get_value('Website Offer', {'name': offer_id}, ['offer_details'])
return frappe.db.get_value("Website Offer", {"name": offer_id}, ["offer_details"])

View File

@@ -34,14 +34,16 @@ class TestWishlist(unittest.TestCase):
# check if wishlist was created and item was added
self.assertTrue(frappe.db.exists("Wishlist", {"user": frappe.session.user}))
self.assertTrue(frappe.db.exists("Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user}))
self.assertTrue(
frappe.db.exists(
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user}
)
)
# add second item to wishlist
add_to_wishlist("Test Phone Series Y")
wishlist_length = frappe.db.get_value(
"Wishlist Item",
{"parent": frappe.session.user},
"count(*)"
"Wishlist Item", {"parent": frappe.session.user}, "count(*)"
)
self.assertEqual(wishlist_length, 2)
@@ -49,9 +51,7 @@ class TestWishlist(unittest.TestCase):
remove_from_wishlist("Test Phone Series Y")
wishlist_length = frappe.db.get_value(
"Wishlist Item",
{"parent": frappe.session.user},
"count(*)"
"Wishlist Item", {"parent": frappe.session.user}, "count(*)"
)
self.assertIsNone(frappe.db.exists("Wishlist Item", {"parent": frappe.session.user}))
self.assertEqual(wishlist_length, 0)
@@ -74,29 +74,44 @@ class TestWishlist(unittest.TestCase):
# check wishlist and its content for users
self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user.name}))
self.assertTrue(frappe.db.exists("Wishlist Item",
{"item_code": "Test Phone Series X", "parent": test_user.name}))
self.assertTrue(
frappe.db.exists(
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}
)
)
self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user_1.name}))
self.assertTrue(frappe.db.exists("Wishlist Item",
{"item_code": "Test Phone Series X", "parent": test_user_1.name}))
self.assertTrue(
frappe.db.exists(
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user_1.name}
)
)
# remove item for second user
remove_from_wishlist("Test Phone Series X")
# make sure item was removed for second user and not first
self.assertFalse(frappe.db.exists("Wishlist Item",
{"item_code": "Test Phone Series X", "parent": test_user_1.name}))
self.assertTrue(frappe.db.exists("Wishlist Item",
{"item_code": "Test Phone Series X", "parent": test_user.name}))
self.assertFalse(
frappe.db.exists(
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user_1.name}
)
)
self.assertTrue(
frappe.db.exists(
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}
)
)
# remove item for first user
frappe.set_user(test_user.name)
remove_from_wishlist("Test Phone Series X")
self.assertFalse(frappe.db.exists("Wishlist Item",
{"item_code": "Test Phone Series X", "parent": test_user.name}))
self.assertFalse(
frappe.db.exists(
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}
)
)
# tear down
frappe.set_user("Administrator")
frappe.get_doc("Wishlist", {"user": test_user.name}).delete()
frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete()
frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete()

View File

@@ -9,6 +9,7 @@ from frappe.model.document import Document
class Wishlist(Document):
pass
@frappe.whitelist()
def add_to_wishlist(item_code):
"""Insert Item into wishlist."""
@@ -20,7 +21,8 @@ def add_to_wishlist(item_code):
"Website Item",
{"item_code": item_code},
["image", "website_warehouse", "name", "web_item_name", "item_name", "item_group", "route"],
as_dict=1)
as_dict=1,
)
wished_item_dict = {
"item_code": item_code,
@@ -30,7 +32,7 @@ def add_to_wishlist(item_code):
"web_item_name": web_item_data.get("web_item_name"),
"image": web_item_data.get("image"),
"warehouse": web_item_data.get("website_warehouse"),
"route": web_item_data.get("route")
"route": web_item_data.get("route"),
}
if not frappe.db.exists("Wishlist", frappe.session.user):
@@ -41,28 +43,20 @@ def add_to_wishlist(item_code):
wishlist.save(ignore_permissions=True)
else:
wishlist = frappe.get_doc("Wishlist", frappe.session.user)
item = wishlist.append('items', wished_item_dict)
item = wishlist.append("items", wished_item_dict)
item.db_insert()
if hasattr(frappe.local, "cookie_manager"):
frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items)))
@frappe.whitelist()
def remove_from_wishlist(item_code):
if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}):
frappe.db.delete(
"Wishlist Item",
{
"item_code": item_code,
"parent": frappe.session.user
}
)
frappe.db.commit() # nosemgrep
frappe.db.delete("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user})
frappe.db.commit() # nosemgrep
wishlist_items = frappe.db.get_values(
"Wishlist Item",
filters={"parent": frappe.session.user}
)
wishlist_items = frappe.db.get_values("Wishlist Item", filters={"parent": frappe.session.user})
if hasattr(frappe.local, "cookie_manager"):
frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist_items)))
frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist_items)))

View File

@@ -9,8 +9,9 @@ from whoosh.query import Prefix
# TODO: Make obsolete
INDEX_NAME = "products"
class ProductSearch(FullTextSearch):
""" Wrapper for WebsiteSearch """
"""Wrapper for WebsiteSearch"""
def get_schema(self):
return Schema(
@@ -29,7 +30,7 @@ class ProductSearch(FullTextSearch):
in www/ and routes from published documents
Returns:
self (object): FullTextSearch Instance
self (object): FullTextSearch Instance
"""
items = get_all_published_items()
documents = [self.get_document_to_index(item) for item in items]
@@ -69,12 +70,12 @@ class ProductSearch(FullTextSearch):
"""Search from the current index
Args:
text (str): String to search for
scope (str, optional): Scope to limit the search. Defaults to None.
limit (int, optional): Limit number of search results. Defaults to 20.
text (str): String to search for
scope (str, optional): Scope to limit the search. Defaults to None.
limit (int, optional): Limit number of search results. Defaults to 20.
Returns:
[List(_dict)]: Search results
[List(_dict)]: Search results
"""
ix = self.get_index()
@@ -111,17 +112,23 @@ class ProductSearch(FullTextSearch):
keyword_highlights=keyword_highlights,
)
def get_all_published_items():
return frappe.get_all("Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code")
return frappe.get_all(
"Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code"
)
def update_index_for_path(path):
search = ProductSearch(INDEX_NAME)
return search.update_index_by_name(path)
def remove_document_from_index(path):
search = ProductSearch(INDEX_NAME)
return search.remove_document_from_index(path)
def build_index_for_all_routes():
search = ProductSearch(INDEX_NAME)
return search.build()

View File

@@ -20,10 +20,10 @@ class ProductFiltersBuilder:
return
fields, filter_data = [], []
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
# filter valid field filters i.e. those that exist in Item
item_meta = frappe.get_meta('Item', cached=True)
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)]
for df in fields:
@@ -36,15 +36,19 @@ class ProductFiltersBuilder:
if include_child:
include_groups = get_child_groups_for_website(self.item_group, include_self=True)
include_groups = [x.name for x in include_groups]
item_or_filters.extend([
["item_group", "in", include_groups],
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
])
item_or_filters.extend(
[
["item_group", "in", include_groups],
["Website Item Group", "item_group", "=", self.item_group], # consider website item groups
]
)
else:
item_or_filters.extend([
["item_group", "=", self.item_group],
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
])
item_or_filters.extend(
[
["item_group", "=", self.item_group],
["Website Item Group", "item_group", "=", self.item_group], # consider website item groups
]
)
# Get link field values attached to published items
item_values = frappe.get_all(
@@ -53,10 +57,10 @@ class ProductFiltersBuilder:
filters=item_filters,
or_filters=item_or_filters,
distinct="True",
pluck=df.fieldname
pluck=df.fieldname,
)
values = list(set(item_values) & link_doctype_values) # intersection of both
values = list(set(item_values) & link_doctype_values) # intersection of both
else:
# table multiselect
values = list(link_doctype_values)
@@ -72,10 +76,10 @@ class ProductFiltersBuilder:
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
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
@@ -91,12 +95,12 @@ class ProductFiltersBuilder:
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
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
@@ -111,12 +115,9 @@ class ProductFiltersBuilder:
result = frappe.get_all(
"Item Variant Attribute",
filters={
"attribute": ["in", attributes],
"attribute_value": ["is", "set"]
},
filters={"attribute": ["in", attributes], "attribute_value": ["is", "set"]},
fields=["attribute", "attribute_value"],
distinct=True
distinct=True,
)
attribute_value_map = {}
@@ -136,11 +137,13 @@ class ProductFiltersBuilder:
# [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
min_range = int(min_discount - (min_range_absolute % 10)) # 20
max_range = int(max_discount - (max_range_absolute % 10)) # 60
min_range = (min_range + 10) if min_range != min_range_absolute else min_range # 30 (upper limit of 25.89 in range of 10)
max_range = (max_range + 10) if max_range != max_range_absolute else max_range # 60
min_range = (
(min_range + 10) if min_range != min_range_absolute else min_range
) # 30 (upper limit of 25.89 in range of 10)
max_range = (max_range + 10) if max_range != max_range_absolute else max_range # 60
for discount in range(min_range, (max_range + 1), 10):
label = f"{discount}% and below"

View File

@@ -13,12 +13,13 @@ class ProductQuery:
"""Query engine for product listing
Attributes:
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
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
"""
def __init__(self):
self.settings = frappe.get_doc("E Commerce Settings")
self.page_length = self.settings.products_per_page or 20
@@ -26,21 +27,33 @@ class ProductQuery:
self.or_filters = []
self.filters = [["published", "=", 1]]
self.fields = [
"web_item_name", "name", "item_name", "item_code", "website_image",
"variant_of", "has_variants", "item_group", "image", "web_long_description",
"short_description", "route", "website_warehouse", "ranking", "on_backorder"
"web_item_name",
"name",
"item_name",
"item_code",
"website_image",
"variant_of",
"has_variants",
"item_group",
"image",
"web_long_description",
"short_description",
"route",
"website_warehouse",
"ranking",
"on_backorder",
]
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
"""
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
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
Returns:
dict: Dict containing items, item count & discount range
dict: Dict containing items, item count & discount range
"""
# track if discounts included in field filters
self.filter_with_discount = bool(fields.get("discount"))
@@ -75,11 +88,7 @@ class ProductQuery:
result = self.filter_results_by_discount(fields, result)
return {
"items": result,
"items_count": count,
"discounts": discounts
}
return {"items": result, "items_count": count, "discounts": discounts}
def query_items(self, start=0):
"""Build a query to fetch Website Items based on field filters."""
@@ -91,8 +100,9 @@ class ProductQuery:
filters=self.filters,
or_filters=self.or_filters,
limit_page_length=184467440737095516,
limit_start=start, # get all items from this offset for total count ahead
order_by="ranking desc")
limit_start=start, # get all items from this offset for total count ahead
order_by="ranking desc",
)
count = len(count_items)
# If discounts included, return all rows.
@@ -108,7 +118,8 @@ class ProductQuery:
or_filters=self.or_filters,
limit_page_length=page_length,
limit_start=start,
order_by="ranking desc")
order_by="ranking desc",
)
return items, count
@@ -127,8 +138,9 @@ class ProductQuery:
filters=[
["published_in_website", "=", 1],
["Item Variant Attribute", "attribute", "=", attribute],
["Item Variant Attribute", "attribute_value", "in", values]
])
["Item Variant Attribute", "attribute_value", "in", values],
],
)
item_codes.append({x.item_code for x in item_code_list})
if item_codes:
@@ -143,21 +155,21 @@ class ProductQuery:
"""Build filters for field values
Args:
filters (dict): Filters
filters (dict): Filters
"""
for field, values in filters.items():
if not values or field == "discount":
continue
# handle multiselect fields in filter addition
meta = frappe.get_meta('Website Item', cached=True)
meta = frappe.get_meta("Website Item", cached=True)
df = meta.get_field(field)
if df.fieldtype == 'Table MultiSelect':
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])
self.filters.append([child_doctype, fields[0].fieldname, "IN", values])
elif isinstance(values, list):
# If value is a list use `IN` query
self.filters.append([field, "in", values])
@@ -168,6 +180,7 @@ class ProductQuery:
def build_item_group_filters(self, item_group):
"Add filters for Item group page and include Website Item Groups."
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
item_group_filters = []
item_group_filters.append(["Website Item", "item_group", "=", item_group])
@@ -188,10 +201,10 @@ class ProductQuery:
"""Query search term in specified fields
Args:
search_term (str): Search candidate
search_term (str): Search candidate
"""
# Default fields to search from
default_fields = {'item_code', 'item_name', 'web_long_description', 'item_group'}
default_fields = {"item_code", "item_name", "web_long_description", "item_group"}
# Get meta search fields
meta = frappe.get_meta("Website Item")
@@ -199,22 +212,24 @@ class ProductQuery:
# Join the meta fields and default fields set
search_fields = default_fields.union(meta_fields)
if frappe.db.count('Website Item', cache=True) > 50000:
search_fields.discard('web_long_description')
if frappe.db.count("Website Item", cache=True) > 50000:
search_fields.discard("web_long_description")
# Build or filters for query
search = '%{}%'.format(search_term)
search = "%{}%".format(search_term)
for field in search_fields:
self.or_filters.append([field, "like", search])
def add_display_details(self, result, discount_list, cart_items):
"""Add price and availability details in result."""
for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get(
"product_info"
)
if product_info and product_info['price']:
if product_info and product_info["price"]:
# update/mutate item and discount_list objects
self.get_price_discount_info(item, product_info['price'], discount_list)
self.get_price_discount_info(item, product_info["price"], discount_list)
if self.settings.show_stock_availability:
self.get_stock_availability(item)
@@ -222,7 +237,9 @@ class ProductQuery:
item.in_cart = item.item_code in cart_items
item.wished = False
if frappe.db.exists("Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}):
if frappe.db.exists(
"Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}
):
item.wished = True
return result, discount_list
@@ -233,13 +250,14 @@ class ProductQuery:
for field in fields:
item[field] = price_object.get(field)
if price_object.get('discount_percent'):
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.discount = price_object.get("formatted_discount_percent") or price_object.get(
"formatted_discount_rate"
)
def get_stock_availability(self, item):
"""Modify item object and add stock details."""
@@ -259,24 +277,29 @@ class ProductQuery:
elif warehouse:
# stock item and has warehouse
actual_qty = frappe.db.get_value(
"Bin",
{"item_code": item.item_code,"warehouse": item.get("website_warehouse")},
"actual_qty")
"Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")}, "actual_qty"
)
item.in_stock = bool(flt(actual_qty))
def get_cart_items(self):
customer = get_customer(silent=True)
if customer:
quotation = frappe.get_all("Quotation", fields=["name"], filters=
{"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
quotation = frappe.get_all(
"Quotation",
fields=["name"],
filters={
"party_name": customer,
"contact_email": frappe.session.user,
"order_type": "Shopping Cart",
"docstatus": 0,
},
order_by="modified desc",
limit_page_length=1,
)
if quotation:
items = frappe.get_all(
"Quotation Item",
fields=["item_code"],
filters={
"parent": quotation[0].get("name")
})
"Quotation Item", fields=["item_code"], filters={"parent": quotation[0].get("name")}
)
items = [row.item_code for row in items]
return items
@@ -285,11 +308,15 @@ class ProductQuery:
def filter_results_by_discount(self, fields, result):
if fields and fields.get("discount"):
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]
result = [
row
for row in result
if row.get("discount_percent") and row.discount_percent <= discount_percent
]
if self.filter_with_discount:
# no limit was added to results while querying
# slice results manually
result[:self.page_length]
result[: self.page_length]
return result

View File

@@ -10,6 +10,7 @@ from erpnext.e_commerce.doctype.website_item.test_website_item import create_reg
test_dependencies = ["Item", "Item Group"]
class TestItemGroupProductDataEngine(unittest.TestCase):
"Test Products & Sub-Category Querying for Product Listing on Item Group Page."
@@ -19,7 +20,7 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
("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")
("Test Mobile E", "_Test Item Group B - 2"),
]
for item in item_codes:
item_code = item[0]
@@ -35,12 +36,14 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
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"
})
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]
@@ -54,48 +57,52 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
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.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"
})
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 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"
})
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
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 - 2", "show_in_website", 0)
result = get_product_filter_data(query_args={
"field_filters": {},
"attribute_filters": {},
"start": 0,
"item_group": "_Test Item Group B"
})
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")))
@@ -104,12 +111,14 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
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"
})
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
@@ -120,19 +129,20 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
"""
Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3).
> _Test Item Group B [Level 1]
> _Test Item Group B - 1 [Level 2]
> _Test Item Group B - 1 - 1 [Level 3]
> _Test Item Group B - 1 [Level 2]
> _Test Item Group B - 1 - 1 [Level 3]
"""
frappe.get_doc({ # create Level 3 nested child group
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B - 1 - 1",
"parent_item_group": "_Test Item Group B - 1"
}).insert()
frappe.get_doc(
{ # create Level 3 nested child group
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B - 1 - 1",
"parent_item_group": "_Test Item Group B - 1",
}
).insert()
create_regular_web_item( # create an item belonging to level 3 item group
"Test Mobile F",
item_args={"item_group": "_Test Item Group B - 1 - 1"}
create_regular_web_item( # create an item belonging to level 3 item group
"Test Mobile F", item_args={"item_group": "_Test Item Group B - 1 - 1"}
)
frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1)
@@ -140,12 +150,14 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
# enable 'include descendants' in Level 1
frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1)
result = get_product_filter_data(query_args={
"field_filters": {},
"attribute_filters": {},
"start": 0,
"item_group": "_Test Item Group B"
})
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]
@@ -155,4 +167,4 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
self.assertIn("Test Mobile A", item_codes)
self.assertIn("Test Mobile C", item_codes)
self.assertIn("Test Mobile E", item_codes)
self.assertIn("Test Mobile F", item_codes)
self.assertIn("Test Mobile F", item_codes)

View File

@@ -14,19 +14,20 @@ from erpnext.e_commerce.product_data_engine.query import ProductQuery
test_dependencies = ["Item", "Item Group"]
class TestProductDataEngine(unittest.TestCase):
"Test Products Querying and Filters for Product Listing."
@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
("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]
@@ -35,17 +36,19 @@ class TestProductDataEngine(unittest.TestCase):
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"}],
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India"
})
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"}],
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
}
)
frappe.local.shopping_cart_settings = None
@classmethod
@@ -55,13 +58,7 @@ class TestProductDataEngine(unittest.TestCase):
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
)
result = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None)
items = result.get("items")
self.assertIsNotNone(items)
@@ -75,13 +72,7 @@ class TestProductDataEngine(unittest.TestCase):
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
)
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
@@ -101,13 +92,7 @@ class TestProductDataEngine(unittest.TestCase):
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
)
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
@@ -152,11 +137,7 @@ class TestProductDataEngine(unittest.TestCase):
engine = ProductQuery()
result = engine.query(
attributes={},
fields=field_filters,
search_term=None,
start=0,
item_group=None
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
)
items = result.get("items")
@@ -188,11 +169,7 @@ class TestProductDataEngine(unittest.TestCase):
attribute_filters = {"Test Size": ["Large"]}
engine = ProductQuery()
result = engine.query(
attributes=attribute_filters,
fields={},
search_term=None,
start=0,
item_group=None
attributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None
)
items = result.get("items")
@@ -209,24 +186,13 @@ class TestProductDataEngine(unittest.TestCase):
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
)
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
)
result = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None)
self.assertTrue(bool(result.get("discounts")))
filter_engine = ProductFiltersBuilder()
@@ -247,16 +213,16 @@ class TestProductDataEngine(unittest.TestCase):
make_web_item_price(item_code="Test 12I Laptop")
make_web_pricing_rule(
title="Test Pricing Rule for Test 12I Laptop", # 10% discount
title="Test Pricing Rule for Test 12I Laptop", # 10% discount
item_code="Test 12I Laptop",
selling=1
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
title="Test Pricing Rule for Test 13I Laptop", # 15% discount
item_code="Test 13I Laptop",
discount_percentage=15,
selling=1
selling=1,
)
setup_e_commerce_settings({"show_price": 1})
@@ -264,11 +230,7 @@ class TestProductDataEngine(unittest.TestCase):
engine = ProductQuery()
result = engine.query(
attributes={},
fields=field_filters,
search_term=None,
start=0,
item_group=None
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
)
items = result.get("items")
@@ -282,15 +244,13 @@ class TestProductDataEngine(unittest.TestCase):
create_variant_web_item()
result = get_product_filter_data(query_args={
"field_filters": {
"item_group": "Products"
},
"attribute_filters": {
"Test Size": ["Large"]
},
"start": 0
})
result = get_product_filter_data(
query_args={
"field_filters": {"item_group": "Products"},
"attribute_filters": {"Test Size": ["Large"]},
"start": 0,
}
)
items = result.get("items")
@@ -301,20 +261,13 @@ class TestProductDataEngine(unittest.TestCase):
"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
})
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
attributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None
)
items = result.get("items")
@@ -322,10 +275,8 @@ class TestProductDataEngine(unittest.TestCase):
self.assertEqual(len(items), 0)
# tear down
setup_e_commerce_settings({
"enable_attribute_filters": 1,
"hide_variants": 0
})
setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0})
def create_variant_web_item():
"Create Variant and Template Website Items."
@@ -333,18 +284,17 @@ def create_variant_web_item():
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.stock.doctype.item.test_item import make_item
make_item("Test Web Item", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{
"attribute": "Test Size"
}
]
})
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)
make_website_item(variant, save=True)

View File

@@ -5,24 +5,27 @@ import frappe
from frappe.utils.redis_wrapper import RedisWrapper
from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
WEBSITE_ITEM_INDEX = 'website_items_index'
WEBSITE_ITEM_KEY_PREFIX = 'website_item:'
WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict'
WEBSITE_ITEM_INDEX = "website_items_index"
WEBSITE_ITEM_KEY_PREFIX = "website_item:"
WEBSITE_ITEM_NAME_AUTOCOMPLETE = "website_items_name_dict"
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = "website_items_category_dict"
def get_indexable_web_fields():
"Return valid fields from Website Item that can be searched for."
web_item_meta = frappe.get_meta("Website Item", cached=True)
valid_fields = filter(
lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"),
web_item_meta.fields)
web_item_meta.fields,
)
return [df.fieldname for df in valid_fields]
def is_search_module_loaded():
try:
cache = frappe.cache()
out = cache.execute_command('MODULE LIST')
out = cache.execute_command("MODULE LIST")
parsed_output = " ".join(
(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
@@ -31,8 +34,10 @@ def is_search_module_loaded():
except Exception:
return False
def if_redisearch_loaded(function):
"Decorator to check if Redisearch is loaded."
def wrapper(*args, **kwargs):
if is_search_module_loaded():
func = function(*args, **kwargs)
@@ -41,8 +46,10 @@ def if_redisearch_loaded(function):
return wrapper
def make_key(key):
return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8")
@if_redisearch_loaded
def create_website_items_index():
@@ -60,14 +67,11 @@ def create_website_items_index():
idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
# Based on e-commerce settings
idx_fields = frappe.db.get_single_value(
'E Commerce Settings',
'search_index_fields'
)
idx_fields = idx_fields.split(',') if idx_fields else []
idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
idx_fields = idx_fields.split(",") if idx_fields else []
if 'web_item_name' in idx_fields:
idx_fields.remove('web_item_name')
if "web_item_name" in idx_fields:
idx_fields.remove("web_item_name")
idx_fields = list(map(to_search_field, idx_fields))
@@ -79,12 +83,14 @@ def create_website_items_index():
reindex_all_web_items()
define_autocomplete_dictionary()
def to_search_field(field):
if field == "tags":
return TagField("tags", separator=",")
return TextField(field)
@if_redisearch_loaded
def insert_item_to_index(website_item_doc):
# Insert item to index
@@ -97,26 +103,30 @@ def insert_item_to_index(website_item_doc):
insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
@if_redisearch_loaded
def insert_to_name_ac(web_name, doc_name):
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
ac.add_suggestions(Suggestion(web_name, payload=doc_name))
def create_web_item_map(website_item_doc):
fields_to_index = get_fields_indexed()
web_item = {}
for f in fields_to_index:
web_item[f] = website_item_doc.get(f) or ''
web_item[f] = website_item_doc.get(f) or ""
return web_item
@if_redisearch_loaded
def update_index_for_item(website_item_doc):
# Reinsert to Cache
insert_item_to_index(website_item_doc)
define_autocomplete_dictionary()
@if_redisearch_loaded
def delete_item_from_index(website_item_doc):
cache = frappe.cache()
@@ -130,26 +140,27 @@ def delete_item_from_index(website_item_doc):
delete_from_ac_dict(website_item_doc)
return True
@if_redisearch_loaded
def delete_from_ac_dict(website_item_doc):
'''Removes this items's name from autocomplete dictionary'''
"""Removes this items's name from autocomplete dictionary"""
cache = frappe.cache()
name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
name_ac.delete(website_item_doc.web_item_name)
@if_redisearch_loaded
def define_autocomplete_dictionary():
"""Creates an autocomplete search dictionary for `name`.
Also creats autocomplete dictionary for `categories` if
checked in E Commerce Settings"""
Also creats autocomplete dictionary for `categories` if
checked in E Commerce Settings"""
cache = frappe.cache()
name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
ac_categories = frappe.db.get_single_value(
'E Commerce Settings',
'show_categories_in_search_autocomplete'
"E Commerce Settings", "show_categories_in_search_autocomplete"
)
# Delete both autocomplete dicts
@@ -160,9 +171,7 @@ def define_autocomplete_dictionary():
return False
items = frappe.get_all(
'Website Item',
fields=['web_item_name', 'item_group'],
filters={"published": 1}
"Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
)
for item in items:
@@ -172,13 +181,10 @@ def define_autocomplete_dictionary():
return True
@if_redisearch_loaded
def reindex_all_web_items():
items = frappe.get_all(
'Website Item',
fields=get_fields_indexed(),
filters={"published": True}
)
items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True})
cache = frappe.cache()
for item in items:
@@ -188,22 +194,22 @@ def reindex_all_web_items():
for k, v in web_item.items():
super(RedisWrapper, cache).hset(key, k, v)
def get_cache_key(name):
name = frappe.scrub(name)
return f"{WEBSITE_ITEM_KEY_PREFIX}{name}"
def get_fields_indexed():
fields_to_index = frappe.db.get_single_value(
'E Commerce Settings',
'search_index_fields'
)
fields_to_index = fields_to_index.split(',') if fields_to_index else []
mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail', 'ranking']
def get_fields_indexed():
fields_to_index = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
fields_to_index = fields_to_index.split(",") if fields_to_index else []
mandatory_fields = ["name", "web_item_name", "route", "thumbnail", "ranking"]
fields_to_index = fields_to_index + mandatory_fields
return fields_to_index
# TODO: Remove later
# # Figure out a way to run this at startup
define_autocomplete_dictionary()

View File

@@ -19,6 +19,7 @@ from erpnext.utilities.product import get_web_item_qty_in_stock
class WebsitePriceListMissingError(frappe.ValidationError):
pass
def set_cart_count(quotation=None):
if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
if not quotation:
@@ -28,6 +29,7 @@ def set_cart_count(quotation=None):
if hasattr(frappe.local, "cookie_manager"):
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
@frappe.whitelist()
def get_cart_quotation(doc=None):
party = get_party()
@@ -47,38 +49,46 @@ def get_cart_quotation(doc=None):
"shipping_addresses": get_shipping_addresses(party),
"billing_addresses": get_billing_addresses(party),
"shipping_rules": get_applicable_shipping_rules(party),
"cart_settings": frappe.get_cached_doc("E Commerce Settings")
"cart_settings": frappe.get_cached_doc("E Commerce Settings"),
}
@frappe.whitelist()
def get_shipping_addresses(party=None):
if not party:
party = get_party()
addresses = get_address_docs(party=party)
return [{"name": address.name, "title": address.address_title, "display": address.display}
for address in addresses if address.address_type == "Shipping"
return [
{"name": address.name, "title": address.address_title, "display": address.display}
for address in addresses
if address.address_type == "Shipping"
]
@frappe.whitelist()
def get_billing_addresses(party=None):
if not party:
party = get_party()
addresses = get_address_docs(party=party)
return [{"name": address.name, "title": address.address_title, "display": address.display}
for address in addresses if address.address_type == "Billing"
return [
{"name": address.name, "title": address.address_title, "display": address.display}
for address in addresses
if address.address_type == "Billing"
]
@frappe.whitelist()
def place_order():
quotation = _get_cart_quotation()
cart_settings = frappe.db.get_value("E Commerce Settings", None,
["company", "allow_items_not_in_stock"], as_dict=1)
cart_settings = frappe.db.get_value(
"E Commerce Settings", None, ["company", "allow_items_not_in_stock"], as_dict=1
)
quotation.company = cart_settings.company
quotation.flags.ignore_permissions = True
quotation.submit()
if quotation.quotation_to == 'Lead' and quotation.party_name:
if quotation.quotation_to == "Lead" and quotation.party_name:
# company used to create customer accounts
frappe.defaults.set_user_default("company", quotation.company)
@@ -86,17 +96,14 @@ def place_order():
frappe.throw(_("Set Shipping Address or Billing Address"))
from erpnext.selling.doctype.quotation.quotation import _make_sales_order
sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True))
sales_order.payment_schedule = []
if not cint(cart_settings.allow_items_not_in_stock):
for item in sales_order.get("items"):
item.warehouse = frappe.db.get_value(
"Website Item",
{
"item_code": item.item_code
},
"website_warehouse"
"Website Item", {"item_code": item.item_code}, "website_warehouse"
)
is_stock_item = frappe.db.get_value("Item", item.item_code, "is_stock_item")
@@ -116,6 +123,7 @@ def place_order():
return sales_order.name
@frappe.whitelist()
def request_for_quotation():
quotation = _get_cart_quotation()
@@ -127,6 +135,7 @@ def request_for_quotation():
quotation.submit()
return quotation.name
@frappe.whitelist()
def update_cart(item_code, qty, additional_notes=None, with_items=False):
quotation = _get_cart_quotation()
@@ -143,12 +152,15 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
else:
quotation_items = quotation.get("items", {"item_code": item_code})
if not quotation_items:
quotation.append("items", {
"doctype": "Quotation Item",
"item_code": item_code,
"qty": qty,
"additional_notes": additional_notes
})
quotation.append(
"items",
{
"doctype": "Quotation Item",
"item_code": item_code,
"qty": qty,
"additional_notes": additional_notes,
},
)
else:
quotation_items[0].qty = qty
quotation_items[0].additional_notes = additional_notes
@@ -168,73 +180,75 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
if cint(with_items):
context = get_cart_quotation(quotation)
return {
"items": frappe.render_template("templates/includes/cart/cart_items.html",
context),
"total": frappe.render_template("templates/includes/cart/cart_items_total.html",
context),
"taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html",
context)
"items": frappe.render_template("templates/includes/cart/cart_items.html", context),
"total": frappe.render_template("templates/includes/cart/cart_items_total.html", context),
"taxes_and_totals": frappe.render_template(
"templates/includes/cart/cart_payment_summary.html", context
),
}
else:
return {
'name': quotation.name
}
return {"name": quotation.name}
@frappe.whitelist()
def get_shopping_cart_menu(context=None):
if not context:
context = get_cart_quotation()
return frappe.render_template('templates/includes/cart/cart_dropdown.html', context)
return frappe.render_template("templates/includes/cart/cart_dropdown.html", context)
@frappe.whitelist()
def add_new_address(doc):
doc = frappe.parse_json(doc)
doc.update({
'doctype': 'Address'
})
doc.update({"doctype": "Address"})
address = frappe.get_doc(doc)
address.save(ignore_permissions=True)
return address
@frappe.whitelist(allow_guest=True)
def create_lead_for_item_inquiry(lead, subject, message):
lead = frappe.parse_json(lead)
lead_doc = frappe.new_doc('Lead')
lead_doc = frappe.new_doc("Lead")
for fieldname in ("lead_name", "company_name", "email_id", "phone"):
lead_doc.set(fieldname, lead.get(fieldname))
lead_doc.set('lead_owner', '')
lead_doc.set("lead_owner", "")
if not frappe.db.exists('Lead Source', 'Product Inquiry'):
frappe.get_doc({
'doctype': 'Lead Source',
'source_name' : 'Product Inquiry'
}).insert(ignore_permissions=True)
if not frappe.db.exists("Lead Source", "Product Inquiry"):
frappe.get_doc({"doctype": "Lead Source", "source_name": "Product Inquiry"}).insert(
ignore_permissions=True
)
lead_doc.set('source', 'Product Inquiry')
lead_doc.set("source", "Product Inquiry")
try:
lead_doc.save(ignore_permissions=True)
except frappe.exceptions.DuplicateEntryError:
frappe.clear_messages()
lead_doc = frappe.get_doc('Lead', {'email_id': lead['email_id']})
lead_doc = frappe.get_doc("Lead", {"email_id": lead["email_id"]})
lead_doc.add_comment('Comment', text='''
lead_doc.add_comment(
"Comment",
text="""
<div>
<h5>{subject}</h5>
<p>{message}</p>
</div>
'''.format(subject=subject, message=message))
""".format(
subject=subject, message=message
),
)
return lead_doc
@frappe.whitelist()
def get_terms_and_conditions(terms_name):
return frappe.db.get_value('Terms and Conditions', terms_name, 'terms')
return frappe.db.get_value("Terms and Conditions", terms_name, "terms")
@frappe.whitelist()
def update_cart_address(address_type, address_name):
@@ -251,31 +265,35 @@ def update_cart_address(address_type, address_name):
quotation.shipping_address_name = address_name
quotation.shipping_address = address_display
quotation.customer_address = quotation.customer_address or address_name
address_doc = next((doc for doc in get_shipping_addresses() if doc["name"] == address_name), None)
address_doc = next(
(doc for doc in get_shipping_addresses() if doc["name"] == address_name), None
)
apply_cart_settings(quotation=quotation)
quotation.flags.ignore_permissions = True
quotation.save()
context = get_cart_quotation(quotation)
context['address'] = address_doc
context["address"] = address_doc
return {
"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
context),
"address": frappe.render_template("templates/includes/cart/address_card.html",
context)
"taxes": frappe.render_template("templates/includes/order/order_taxes.html", context),
"address": frappe.render_template("templates/includes/cart/address_card.html", context),
}
def guess_territory():
territory = None
geoip_country = frappe.session.get("session_country")
if geoip_country:
territory = frappe.db.get_value("Territory", geoip_country)
return territory or \
frappe.db.get_value("E Commerce Settings", None, "territory") or \
get_root_of("Territory")
return (
territory
or frappe.db.get_value("E Commerce Settings", None, "territory")
or get_root_of("Territory")
)
def decorate_quotation_doc(doc):
for d in doc.get("items", []):
@@ -288,50 +306,56 @@ def decorate_quotation_doc(doc):
"Item",
filters={"item_code": item_code},
fieldname=["variant_of", "item_name", "image"],
as_dict=True
as_dict=True,
)[0]
item_code = variant_data.variant_of
fields = fields[1:]
d.web_item_name = variant_data.item_name
if variant_data.image: # get image from variant or template web item
if variant_data.image: # get image from variant or template web item
d.thumbnail = variant_data.image
fields = fields[2:]
d.update(frappe.db.get_value(
"Website Item",
{"item_code": item_code},
fields,
as_dict=True)
)
d.update(frappe.db.get_value("Website Item", {"item_code": item_code}, fields, as_dict=True))
return doc
def _get_cart_quotation(party=None):
'''Return the open Quotation of type "Shopping Cart" or make a new one'''
"""Return the open Quotation of type "Shopping Cart" or make a new one"""
if not party:
party = get_party()
quotation = frappe.get_all("Quotation", fields=["name"], filters=
{"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
quotation = frappe.get_all(
"Quotation",
fields=["name"],
filters={
"party_name": party.name,
"contact_email": frappe.session.user,
"order_type": "Shopping Cart",
"docstatus": 0,
},
order_by="modified desc",
limit_page_length=1,
)
if quotation:
qdoc = frappe.get_doc("Quotation", quotation[0].name)
else:
company = frappe.db.get_value("E Commerce Settings", None, ["company"])
qdoc = frappe.get_doc({
"doctype": "Quotation",
"naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-",
"quotation_to": party.doctype,
"company": company,
"order_type": "Shopping Cart",
"status": "Draft",
"docstatus": 0,
"__islocal": 1,
"party_name": party.name
})
qdoc = frappe.get_doc(
{
"doctype": "Quotation",
"naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-",
"quotation_to": party.doctype,
"company": company,
"order_type": "Shopping Cart",
"status": "Draft",
"docstatus": 0,
"__islocal": 1,
"party_name": party.name,
}
)
qdoc.contact_person = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
qdoc.contact_email = frappe.session.user
@@ -342,6 +366,7 @@ def _get_cart_quotation(party=None):
return qdoc
def update_party(fullname, company_name=None, mobile_no=None, phone=None):
party = get_party()
@@ -369,6 +394,7 @@ def update_party(fullname, company_name=None, mobile_no=None, phone=None):
qdoc.flags.ignore_permissions = True
qdoc.save()
def apply_cart_settings(party=None, quotation=None):
if not party:
party = get_party()
@@ -385,14 +411,16 @@ def apply_cart_settings(party=None, quotation=None):
_apply_shipping_rule(party, quotation, cart_settings)
def set_price_list_and_rate(quotation, cart_settings):
"""set price list based on billing territory"""
_set_price_list(cart_settings, quotation)
# reset values
quotation.price_list_currency = quotation.currency = \
quotation.plc_conversion_rate = quotation.conversion_rate = None
quotation.price_list_currency = (
quotation.currency
) = quotation.plc_conversion_rate = quotation.conversion_rate = None
for item in quotation.get("items"):
item.price_list_rate = item.discount_percentage = item.rate = item.amount = None
@@ -403,9 +431,11 @@ def set_price_list_and_rate(quotation, cart_settings):
# set it in cookies for using in product page
frappe.local.cookie_manager.set_cookie("selling_price_list", quotation.selling_price_list)
def _set_price_list(cart_settings, quotation=None):
"""Set price list based on customer or shopping cart default"""
from erpnext.accounts.party import get_default_price_list
party_name = quotation.get("party_name") if quotation else get_party().get("name")
selling_price_list = None
@@ -422,23 +452,33 @@ def _set_price_list(cart_settings, quotation=None):
return selling_price_list
def set_taxes(quotation, cart_settings):
"""set taxes based on billing territory"""
from erpnext.accounts.party import set_taxes
customer_group = frappe.db.get_value("Customer", quotation.party_name, "customer_group")
quotation.taxes_and_charges = set_taxes(quotation.party_name, "Customer",
quotation.transaction_date, quotation.company, customer_group=customer_group, supplier_group=None,
tax_category=quotation.tax_category, billing_address=quotation.customer_address,
shipping_address=quotation.shipping_address_name, use_for_shopping_cart=1)
#
# # clear table
quotation.taxes_and_charges = set_taxes(
quotation.party_name,
"Customer",
quotation.transaction_date,
quotation.company,
customer_group=customer_group,
supplier_group=None,
tax_category=quotation.tax_category,
billing_address=quotation.customer_address,
shipping_address=quotation.shipping_address_name,
use_for_shopping_cart=1,
)
#
# # clear table
quotation.set("taxes", [])
#
# # append taxes
#
# # append taxes
quotation.append_taxes_from_master()
def get_party(user=None):
if not user:
user = frappe.session.user
@@ -447,14 +487,14 @@ def get_party(user=None):
party = None
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
contact = frappe.get_doc("Contact", contact_name)
if contact.links:
party_doctype = contact.links[0].link_doctype
party = contact.links[0].link_name
cart_settings = frappe.get_doc("E Commerce Settings")
debtors_account = ''
debtors_account = ""
if cart_settings.enable_checkout:
debtors_account = get_debtors_account(cart_settings)
@@ -468,57 +508,62 @@ def get_party(user=None):
raise frappe.Redirect
customer = frappe.new_doc("Customer")
fullname = get_fullname(user)
customer.update({
"customer_name": fullname,
"customer_type": "Individual",
"customer_group": get_shopping_cart_settings().default_customer_group,
"territory": get_root_of("Territory")
})
customer.update(
{
"customer_name": fullname,
"customer_type": "Individual",
"customer_group": get_shopping_cart_settings().default_customer_group,
"territory": get_root_of("Territory"),
}
)
if debtors_account:
customer.update({
"accounts": [{
"company": cart_settings.company,
"account": debtors_account
}]
})
customer.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]})
customer.flags.ignore_mandatory = True
customer.insert(ignore_permissions=True)
contact = frappe.new_doc("Contact")
contact.update({
"first_name": fullname,
"email_ids": [{"email_id": user, "is_primary": 1}]
})
contact.append('links', dict(link_doctype='Customer', link_name=customer.name))
contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]})
contact.append("links", dict(link_doctype="Customer", link_name=customer.name))
contact.flags.ignore_mandatory = True
contact.insert(ignore_permissions=True)
return customer
def get_debtors_account(cart_settings):
if not cart_settings.payment_gateway_account:
frappe.throw(_("Payment Gateway Account not set"), _("Mandatory"))
payment_gateway_account_currency = \
frappe.get_doc("Payment Gateway Account", cart_settings.payment_gateway_account).currency
payment_gateway_account_currency = frappe.get_doc(
"Payment Gateway Account", cart_settings.payment_gateway_account
).currency
account_name = _("Debtors ({0})").format(payment_gateway_account_currency)
debtors_account_name = get_account_name("Receivable", "Asset", is_group=0,\
account_currency=payment_gateway_account_currency, company=cart_settings.company)
debtors_account_name = get_account_name(
"Receivable",
"Asset",
is_group=0,
account_currency=payment_gateway_account_currency,
company=cart_settings.company,
)
if not debtors_account_name:
debtors_account = frappe.get_doc({
"doctype": "Account",
"account_type": "Receivable",
"root_type": "Asset",
"is_group": 0,
"parent_account": get_account_name(root_type="Asset", is_group=1, company=cart_settings.company),
"account_name": account_name,
"currency": payment_gateway_account_currency
}).insert(ignore_permissions=True)
debtors_account = frappe.get_doc(
{
"doctype": "Account",
"account_type": "Receivable",
"root_type": "Asset",
"is_group": 0,
"parent_account": get_account_name(
root_type="Asset", is_group=1, company=cart_settings.company
),
"account_name": account_name,
"currency": payment_gateway_account_currency,
}
).insert(ignore_permissions=True)
return debtors_account.name
@@ -526,26 +571,31 @@ def get_debtors_account(cart_settings):
return debtors_account_name
def get_address_docs(doctype=None, txt=None, filters=None, limit_start=0, limit_page_length=20,
party=None):
def get_address_docs(
doctype=None, txt=None, filters=None, limit_start=0, limit_page_length=20, party=None
):
if not party:
party = get_party()
if not party:
return []
address_names = frappe.db.get_all('Dynamic Link', fields=('parent'),
filters=dict(parenttype='Address', link_doctype=party.doctype, link_name=party.name))
address_names = frappe.db.get_all(
"Dynamic Link",
fields=("parent"),
filters=dict(parenttype="Address", link_doctype=party.doctype, link_name=party.name),
)
out = []
for a in address_names:
address = frappe.get_doc('Address', a.parent)
address = frappe.get_doc("Address", a.parent)
address.display = get_address_display(address.as_dict())
out.append(address)
return out
@frappe.whitelist()
def apply_shipping_rule(shipping_rule):
quotation = _get_cart_quotation()
@@ -559,6 +609,7 @@ def apply_shipping_rule(shipping_rule):
return get_cart_quotation(quotation)
def _apply_shipping_rule(party=None, quotation=None, cart_settings=None):
if not quotation.shipping_rule:
shipping_rules = get_shipping_rules(quotation, cart_settings)
@@ -573,6 +624,7 @@ def _apply_shipping_rule(party=None, quotation=None, cart_settings=None):
quotation.run_method("apply_shipping_rule")
quotation.run_method("calculate_taxes_and_totals")
def get_applicable_shipping_rules(party=None, quotation=None):
shipping_rules = get_shipping_rules(quotation)
@@ -581,6 +633,7 @@ def get_applicable_shipping_rules(party=None, quotation=None):
# we need this in sorted order as per the position of the rule in the settings page
return [[rule, rule] for rule in shipping_rules]
def get_shipping_rules(quotation=None, cart_settings=None):
if not quotation:
quotation = _get_cart_quotation()
@@ -593,26 +646,24 @@ def get_shipping_rules(quotation=None, cart_settings=None):
sr = frappe.qb.DocType("Shipping Rule")
query = (
frappe.qb.from_(sr_country)
.join(sr).on(sr.name == sr_country.parent)
.join(sr)
.on(sr.name == sr_country.parent)
.select(sr.name)
.distinct()
.where(
(sr_country.country == country)
& (sr.disabled != 1)
)
.where((sr_country.country == country) & (sr.disabled != 1))
)
result = query.run(as_list=True)
shipping_rules = [x[0] for x in result]
return shipping_rules
def get_address_territory(address_name):
"""Tries to match city, state and country of address to existing territory"""
territory = None
if address_name:
address_fields = frappe.db.get_value("Address", address_name,
["city", "state", "country"])
address_fields = frappe.db.get_value("Address", address_name, ["city", "state", "country"])
for value in address_fields:
territory = frappe.db.get_value("Territory", value)
if territory:
@@ -620,9 +671,11 @@ def get_address_territory(address_name):
return territory
def show_terms(doc):
return doc.tc_name
@frappe.whitelist(allow_guest=True)
def apply_coupon_code(applied_code, applied_referral_sales_partner):
quotation = True
@@ -630,13 +683,14 @@ def apply_coupon_code(applied_code, applied_referral_sales_partner):
if not applied_code:
frappe.throw(_("Please enter a coupon code"))
coupon_list = frappe.get_all('Coupon Code', filters={'coupon_code': applied_code})
coupon_list = frappe.get_all("Coupon Code", filters={"coupon_code": applied_code})
if not coupon_list:
frappe.throw(_("Please enter a valid coupon code"))
coupon_name = coupon_list[0].name
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
validate_coupon_code(coupon_name)
quotation = _get_cart_quotation()
quotation.coupon_code = coupon_name
@@ -644,7 +698,9 @@ def apply_coupon_code(applied_code, applied_referral_sales_partner):
quotation.save()
if applied_referral_sales_partner:
sales_partner_list = frappe.get_all('Sales Partner', filters={'referral_code': applied_referral_sales_partner})
sales_partner_list = frappe.get_all(
"Sales Partner", filters={"referral_code": applied_referral_sales_partner}
)
if sales_partner_list:
sales_partner_name = sales_partner_list[0].name
quotation.referral_sales_partner = sales_partner_name

View File

@@ -22,16 +22,17 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
cart_settings = get_shopping_cart_settings()
if not cart_settings.enabled:
# return settings even if cart is disabled
return frappe._dict({
"product_info": {},
"cart_settings": cart_settings
})
return frappe._dict({"product_info": {}, "cart_settings": cart_settings})
cart_quotation = frappe._dict()
if not skip_quotation_creation:
cart_quotation = _get_cart_quotation()
selling_price_list = cart_quotation.get("selling_price_list") if cart_quotation else _set_price_list(cart_settings, None)
selling_price_list = (
cart_quotation.get("selling_price_list")
if cart_quotation
else _set_price_list(cart_settings, None)
)
price = {}
if cart_settings.show_price:
@@ -40,10 +41,7 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
# If not logged in, check if price is hidden for guest.
if not is_guest or not cart_settings.hide_price_for_guest:
price = get_price(
item_code,
selling_price_list,
cart_settings.default_customer_group,
cart_settings.company
item_code, selling_price_list, cart_settings.default_customer_group, cart_settings.company
)
stock_status = None
@@ -59,7 +57,7 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
"price": price,
"qty": 0,
"uom": frappe.db.get_value("Item", item_code, "stock_uom"),
"sales_uom": frappe.db.get_value("Item", item_code, "sales_uom")
"sales_uom": frappe.db.get_value("Item", item_code, "sales_uom"),
}
if stock_status:
@@ -67,7 +65,11 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
product_info["on_backorder"] = True
else:
product_info["stock_qty"] = stock_status.stock_qty
product_info["in_stock"] = stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse")
product_info["in_stock"] = (
stock_status.in_stock
if stock_status.is_stock_item
else get_non_stock_item_status(item_code, "website_warehouse")
)
product_info["show_stock_qty"] = show_quantity_in_website()
if product_info["price"]:
@@ -76,14 +78,14 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
if item:
product_info["qty"] = item[0].qty
return frappe._dict({
"product_info": product_info,
"cart_settings": cart_settings
})
return frappe._dict({"product_info": product_info, "cart_settings": cart_settings})
def set_product_info_for_website(item):
"""set product price uom for website"""
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get("product_info")
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get(
"product_info"
)
if product_info:
item.update(product_info)

View File

@@ -22,18 +22,19 @@ from erpnext.tests.utils import create_test_contact_and_address
class TestShoppingCart(unittest.TestCase):
"""
Note:
Shopping Cart == Quotation
Note:
Shopping Cart == Quotation
"""
def setUp(self):
frappe.set_user("Administrator")
create_test_contact_and_address()
self.enable_shopping_cart()
if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
if not frappe.db.exists("Website Item", {"item_code": "_Test Item 2"}):
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
def tearDown(self):
frappe.db.rollback()
@@ -50,8 +51,10 @@ class TestShoppingCart(unittest.TestCase):
# test if lead is created and quotation with new lead is fetched
quotation = _get_cart_quotation()
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(quotation.contact_person,
frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")))
self.assertEqual(
quotation.contact_person,
frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")),
)
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
@@ -65,7 +68,9 @@ class TestShoppingCart(unittest.TestCase):
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer")
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
validate_quotation()
self.login_as_customer()
@@ -131,46 +136,55 @@ class TestShoppingCart(unittest.TestCase):
from erpnext.accounts.party import set_taxes
tax_rule_master = set_taxes(quotation.party_name, "Customer",
quotation.transaction_date, quotation.company, customer_group=None, supplier_group=None,
tax_category=quotation.tax_category, billing_address=quotation.customer_address,
shipping_address=quotation.shipping_address_name, use_for_shopping_cart=1)
tax_rule_master = set_taxes(
quotation.party_name,
"Customer",
quotation.transaction_date,
quotation.company,
customer_group=None,
supplier_group=None,
tax_category=quotation.tax_category,
billing_address=quotation.customer_address,
shipping_address=quotation.shipping_address_name,
use_for_shopping_cart=1,
)
self.assertEqual(quotation.taxes_and_charges, tax_rule_master)
self.assertEqual(quotation.total_taxes_and_charges, 1000.0)
self.remove_test_quotation(quotation)
@change_settings("E Commerce Settings",{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
@change_settings(
"E Commerce Settings",
{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1,
},
)
def test_add_item_variant_without_web_item_to_cart(self):
"Test adding Variants having no Website Items in cart via Template Web Item."
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.stock.doctype.item.test_item import make_item
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small", "Test Colour": "Red"
})
template_item = make_item(
"Test-Tshirt-Temp",
{
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [{"attribute": "Test Size"}, {"attribute": "Test Colour"}],
},
)
variant = create_variant("Test-Tshirt-Temp", {"Test Size": "Small", "Test Colour": "Red"})
variant.save()
make_website_item(template_item) # publish template not variant
make_website_item(template_item) # publish template not variant
update_cart("Test-Tshirt-Temp-S-R", 1)
cart = get_cart_quotation() # test if cart page gets data without errors
cart = get_cart_quotation() # test if cart page gets data without errors
doc = cart.get("doc")
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
@@ -178,16 +192,14 @@ class TestShoppingCart(unittest.TestCase):
# test if items are rendered without error
frappe.render_template("templates/includes/cart/cart_items.html", cart)
@change_settings("E Commerce Settings",{
"save_quotations_as_draft": 1
})
@change_settings("E Commerce Settings", {"save_quotations_as_draft": 1})
def test_cart_without_checkout_and_draft_quotation(self):
"Test impact of 'save_quotations_as_draft' checkbox."
frappe.local.shopping_cart_settings = None
# add item to cart
update_cart("_Test Item", 1)
quote_name = request_for_quotation() # Request for Quote
quote_name = request_for_quotation() # Request for Quote
quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
self.assertEqual(quote_doctstatus, 0)
@@ -195,7 +207,7 @@ class TestShoppingCart(unittest.TestCase):
frappe.db.set_value("E Commerce Settings", None, "save_quotations_as_draft", 0)
frappe.local.shopping_cart_settings = None
update_cart("_Test Item", 1)
quote_name = request_for_quotation() # Request for Quote
quote_name = request_for_quotation() # Request for Quote
quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
self.assertEqual(quote_doctstatus, 1)
@@ -219,16 +231,13 @@ class TestShoppingCart(unittest.TestCase):
"contact_email": frappe.session.user,
"selling_price_list": "_Test Price List Rest of the World",
"currency": "USD",
"taxes_and_charges" : "_Test Tax 1 - _TC",
"conversion_rate":1,
"transaction_date" : nowdate(),
"valid_till" : add_months(nowdate(), 1),
"items": [{
"item_code": "_Test Item",
"qty": 1
}],
"taxes_and_charges": "_Test Tax 1 - _TC",
"conversion_rate": 1,
"transaction_date": nowdate(),
"valid_till": add_months(nowdate(), 1),
"items": [{"item_code": "_Test Item", "qty": 1}],
"taxes": frappe.get_doc("Sales Taxes and Charges Template", "_Test Tax 1 - _TC").taxes,
"company": "_Test Company"
"company": "_Test Company",
}
quotation.update(values)
@@ -245,29 +254,36 @@ class TestShoppingCart(unittest.TestCase):
def enable_shopping_cart(self):
settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.update({
"enabled": 1,
"company": "_Test Company",
"default_customer_group": "_Test Customer Group",
"quotation_series": "_T-Quotation-",
"price_list": "_Test Price List India"
})
settings.update(
{
"enabled": 1,
"company": "_Test Company",
"default_customer_group": "_Test Customer Group",
"quotation_series": "_T-Quotation-",
"price_list": "_Test Price List India",
}
)
# insert item price
if not frappe.db.get_value("Item Price", {"price_list": "_Test Price List India",
"item_code": "_Test Item"}):
frappe.get_doc({
"doctype": "Item Price",
"price_list": "_Test Price List India",
"item_code": "_Test Item",
"price_list_rate": 10
}).insert()
frappe.get_doc({
"doctype": "Item Price",
"price_list": "_Test Price List India",
"item_code": "_Test Item 2",
"price_list_rate": 20
}).insert()
if not frappe.db.get_value(
"Item Price", {"price_list": "_Test Price List India", "item_code": "_Test Item"}
):
frappe.get_doc(
{
"doctype": "Item Price",
"price_list": "_Test Price List India",
"item_code": "_Test Item",
"price_list_rate": 10,
}
).insert()
frappe.get_doc(
{
"doctype": "Item Price",
"price_list": "_Test Price List India",
"item_code": "_Test Item 2",
"price_list_rate": 20,
}
).insert()
settings.save()
frappe.local.shopping_cart_settings = None
@@ -282,31 +298,49 @@ class TestShoppingCart(unittest.TestCase):
self.create_user_if_not_exists("test_cart_user@example.com")
frappe.set_user("test_cart_user@example.com")
def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"):
def login_as_customer(
self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"
):
self.create_user_if_not_exists(email, name)
frappe.set_user(email)
def clear_existing_quotations(self):
quotations = frappe.get_all("Quotation", filters={
"party_name": get_party().name,
"order_type": "Shopping Cart",
"docstatus": 0
}, order_by="modified desc", pluck="name")
quotations = frappe.get_all(
"Quotation",
filters={"party_name": get_party().name, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc",
pluck="name",
)
for quotation in quotations:
frappe.delete_doc("Quotation", quotation, ignore_permissions=True, force=True)
def create_user_if_not_exists(self, email, first_name = None):
def create_user_if_not_exists(self, email, first_name=None):
if frappe.db.exists("User", email):
return
frappe.get_doc({
"doctype": "User",
"user_type": "Website User",
"email": email,
"send_welcome_email": 0,
"first_name": first_name or email.split("@")[0]
}).insert(ignore_permissions=True)
frappe.get_doc(
{
"doctype": "User",
"user_type": "Website User",
"email": email,
"send_welcome_email": 0,
"first_name": first_name or email.split("@")[0],
}
).insert(ignore_permissions=True)
test_dependencies = ["Sales Taxes and Charges Template", "Price List", "Item Price", "Shipping Rule", "Currency Exchange",
"Customer Group", "Lead", "Customer", "Contact", "Address", "Item", "Tax Rule"]
test_dependencies = [
"Sales Taxes and Charges Template",
"Price List",
"Item Price",
"Shipping Rule",
"Currency Exchange",
"Customer Group",
"Lead",
"Customer",
"Contact",
"Address",
"Item",
"Tax Rule",
]

View File

@@ -6,12 +6,15 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import i
def show_cart_count():
if (is_cart_enabled() and
frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User"):
if (
is_cart_enabled()
and frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User"
):
return True
return False
def set_cart_count(login_manager):
# since this is run only on hooks login event
# make sure user is already a customer
@@ -28,21 +31,24 @@ def set_cart_count(login_manager):
# cart count is calculated from this quotation's items
set_cart_count()
def clear_cart_count(login_manager):
if show_cart_count():
frappe.local.cookie_manager.delete_cookie("cart_count")
def update_website_context(context):
cart_enabled = is_cart_enabled()
context["shopping_cart_enabled"] = cart_enabled
def is_customer():
if frappe.session.user and frappe.session.user != "Guest":
contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
contact = frappe.get_doc("Contact", contact_name)
for link in contact.links:
if link.link_doctype == 'Customer':
if link.link_doctype == "Customer":
return True
return False

View File

@@ -6,63 +6,60 @@ class ItemVariantsCacheManager:
self.item_code = item_code
def get_item_variants_data(self):
val = frappe.cache().hget('item_variants_data', self.item_code)
val = frappe.cache().hget("item_variants_data", self.item_code)
if not val:
self.build_cache()
return frappe.cache().hget('item_variants_data', self.item_code)
return frappe.cache().hget("item_variants_data", self.item_code)
def get_attribute_value_item_map(self):
val = frappe.cache().hget('attribute_value_item_map', self.item_code)
val = frappe.cache().hget("attribute_value_item_map", self.item_code)
if not val:
self.build_cache()
return frappe.cache().hget('attribute_value_item_map', self.item_code)
return frappe.cache().hget("attribute_value_item_map", self.item_code)
def get_item_attribute_value_map(self):
val = frappe.cache().hget('item_attribute_value_map', self.item_code)
val = frappe.cache().hget("item_attribute_value_map", self.item_code)
if not val:
self.build_cache()
return frappe.cache().hget('item_attribute_value_map', self.item_code)
return frappe.cache().hget("item_attribute_value_map", self.item_code)
def get_optional_attributes(self):
val = frappe.cache().hget('optional_attributes', self.item_code)
val = frappe.cache().hget("optional_attributes", self.item_code)
if not val:
self.build_cache()
return frappe.cache().hget('optional_attributes', self.item_code)
return frappe.cache().hget("optional_attributes", self.item_code)
def get_ordered_attribute_values(self):
val = frappe.cache().get_value('ordered_attribute_values_map')
if val: return val
val = frappe.cache().get_value("ordered_attribute_values_map")
if val:
return val
all_attribute_values = frappe.get_all('Item Attribute Value',
['attribute_value', 'idx', 'parent'], order_by='idx asc')
all_attribute_values = frappe.get_all(
"Item Attribute Value", ["attribute_value", "idx", "parent"], order_by="idx asc"
)
ordered_attribute_values_map = frappe._dict({})
for d in all_attribute_values:
ordered_attribute_values_map.setdefault(d.parent, []).append(d.attribute_value)
frappe.cache().set_value('ordered_attribute_values_map', ordered_attribute_values_map)
frappe.cache().set_value("ordered_attribute_values_map", ordered_attribute_values_map)
return ordered_attribute_values_map
def build_cache(self):
parent_item_code = self.item_code
attributes = [
a.attribute for a in frappe.get_all(
'Item Variant Attribute',
{'parent': parent_item_code},
['attribute'],
order_by='idx asc'
a.attribute
for a in frappe.get_all(
"Item Variant Attribute", {"parent": parent_item_code}, ["attribute"], order_by="idx asc"
)
]
@@ -71,13 +68,11 @@ class ItemVariantsCacheManager:
item = frappe.qb.DocType("Item")
query = (
frappe.qb.from_(iva)
.join(item).on(item.name == iva.parent)
.select(
iva.parent, iva.attribute, iva.attribute_value
).where(
(iva.variant_of == parent_item_code)
& (item.disabled == 0)
).orderby(iva.name)
.join(item)
.on(item.name == iva.parent)
.select(iva.parent, iva.attribute, iva.attribute_value)
.where((iva.variant_of == parent_item_code) & (item.disabled == 0))
.orderby(iva.name)
)
item_variants_data = query.run()
@@ -97,13 +92,18 @@ class ItemVariantsCacheManager:
if attribute not in attr_dict:
optional_attributes.add(attribute)
frappe.cache().hset('attribute_value_item_map', parent_item_code, attribute_value_item_map)
frappe.cache().hset('item_attribute_value_map', parent_item_code, item_attribute_value_map)
frappe.cache().hset('item_variants_data', parent_item_code, item_variants_data)
frappe.cache().hset('optional_attributes', parent_item_code, optional_attributes)
frappe.cache().hset("attribute_value_item_map", parent_item_code, attribute_value_item_map)
frappe.cache().hset("item_attribute_value_map", parent_item_code, item_attribute_value_map)
frappe.cache().hset("item_variants_data", parent_item_code, item_variants_data)
frappe.cache().hset("optional_attributes", parent_item_code, optional_attributes)
def clear_cache(self):
keys = ['attribute_value_item_map', 'item_attribute_value_map', 'item_variants_data', 'optional_attributes']
keys = [
"attribute_value_item_map",
"item_attribute_value_map",
"item_variants_data",
"optional_attributes",
]
for key in keys:
frappe.cache().hdel(key, self.item_code)
@@ -114,15 +114,17 @@ class ItemVariantsCacheManager:
def build_cache(item_code):
frappe.cache().hset('item_cache_build_in_progress', item_code, 1)
frappe.cache().hset("item_cache_build_in_progress", item_code, 1)
i = ItemVariantsCacheManager(item_code)
i.build_cache()
frappe.cache().hset('item_cache_build_in_progress', item_code, 0)
frappe.cache().hset("item_cache_build_in_progress", item_code, 0)
def enqueue_build_cache(item_code):
if frappe.cache().hget('item_cache_build_in_progress', item_code):
if frappe.cache().hget("item_cache_build_in_progress", item_code):
return
frappe.enqueue(
"erpnext.e_commerce.variant_selector.item_variants_cache.build_cache",
item_code=item_code, queue='long'
item_code=item_code,
queue="long",
)

View File

@@ -11,41 +11,43 @@ from erpnext.stock.doctype.item.test_item import make_item
test_dependencies = ["Item"]
class TestVariantSelector(FrappeTestCase):
class TestVariantSelector(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
template_item = make_item(
"Test-Tshirt-Temp",
{
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [{"attribute": "Test Size"}, {"attribute": "Test Colour"}],
},
)
# create L-R, L-G, M-R, M-G and S-R
for size in ("Large", "Medium",):
for colour in ("Red", "Green",):
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": size, "Test Colour": colour
})
for size in (
"Large",
"Medium",
):
for colour in (
"Red",
"Green",
):
variant = create_variant("Test-Tshirt-Temp", {"Test Size": size, "Test Colour": colour})
variant.save()
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small", "Test Colour": "Red"
})
variant = create_variant("Test-Tshirt-Temp", {"Test Size": "Small", "Test Colour": "Red"})
variant.save()
make_website_item(template_item) # publish template not variants
make_website_item(template_item) # publish template not variants
def test_item_attributes(self):
"""
Test if the right attributes are fetched in the popup.
(Attributes must only come from active items)
Test if the right attributes are fetched in the popup.
(Attributes must only come from active items)
Attribute selection must not be linked to Website Items.
Attribute selection must not be linked to Website Items.
"""
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
@@ -53,14 +55,14 @@ class TestVariantSelector(FrappeTestCase):
self.assertEqual(attr_data[0]["attribute"], "Test Size")
self.assertEqual(attr_data[1]["attribute"], "Test Colour")
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
# disable small red tshirt, now there are no small tshirts.
# but there are some red tshirts
small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
small_variant.disabled = 1
small_variant.save() # trigger cache rebuild
small_variant.save() # trigger cache rebuild
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
@@ -73,14 +75,16 @@ class TestVariantSelector(FrappeTestCase):
def test_next_item_variant_values(self):
"""
Test if on selecting an attribute value, the next possible values
are filtered accordingly.
Values that dont apply should not be fetched.
E.g.
There is a ** Small-Red ** Tshirt. No other colour in this size.
On selecting ** Small **, only ** Red ** should be selectable next.
Test if on selecting an attribute value, the next possible values
are filtered accordingly.
Values that dont apply should not be fetched.
E.g.
There is a ** Small-Red ** Tshirt. No other colour in this size.
On selecting ** Small **, only ** Red ** should be selectable next.
"""
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
next_values = get_next_attribute_and_values(
"Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"}
)
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
filtered_items = next_values["filtered_items"]
@@ -91,30 +95,31 @@ class TestVariantSelector(FrappeTestCase):
def test_exact_match_with_price(self):
"""
Test price fetching and matching of variant without Website Item
Test price fetching and matching of variant without Website Item
"""
from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
frappe.set_user("Administrator")
setup_e_commerce_settings({
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
setup_e_commerce_settings(
{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1,
}
)
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
frappe.local.shopping_cart_settings = None # clear cached settings values
frappe.local.shopping_cart_settings = None # clear cached settings values
next_values = get_next_attribute_and_values(
"Test-Tshirt-Temp",
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
"Test-Tshirt-Temp", selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
)
print(">>>>", next_values)
price_info = next_values["product_info"]["price"]
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0], "Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0], "Test-Tshirt-Temp-S-R")
self.assertEqual(price_info["price_list_rate"], 100.0)
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")

View File

@@ -24,18 +24,18 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
wheres = []
query_values = []
for attribute_value in attribute_values:
wheres.append('( attribute = %s and attribute_value = %s )')
wheres.append("( attribute = %s and attribute_value = %s )")
query_values += [attribute, attribute_value]
attribute_query = ' or '.join(wheres)
attribute_query = " or ".join(wheres)
if template_item_code:
variant_of_query = 'AND t2.variant_of = %s'
variant_of_query = "AND t2.variant_of = %s"
query_values.append(template_item_code)
else:
variant_of_query = ''
variant_of_query = ""
query = '''
query = """
SELECT
t1.parent
FROM
@@ -58,20 +58,23 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
t1.parent
ORDER BY
NULL
'''.format(attribute_query=attribute_query, variant_of_query=variant_of_query)
""".format(
attribute_query=attribute_query, variant_of_query=variant_of_query
)
item_codes = set([r[0] for r in frappe.db.sql(query, query_values)]) # nosemgrep
item_codes = set([r[0] for r in frappe.db.sql(query, query_values)]) # nosemgrep
items.append(item_codes)
res = list(set.intersection(*items))
return res
@frappe.whitelist(allow_guest=True)
def get_attributes_and_values(item_code):
'''Build a list of attributes and their possible values.
"""Build a list of attributes and their possible values.
This will ignore the values upon selection of which there cannot exist one item.
'''
"""
item_cache = ItemVariantsCacheManager(item_code)
item_variants_data = item_cache.get_item_variants_data()
@@ -83,8 +86,9 @@ def get_attributes_and_values(item_code):
if attribute in attribute_list:
valid_options.setdefault(attribute, set()).add(attribute_value)
item_attribute_values = frappe.db.get_all('Item Attribute Value',
['parent', 'attribute_value', 'idx'], order_by='parent asc, idx asc')
item_attribute_values = frappe.db.get_all(
"Item Attribute Value", ["parent", "attribute_value", "idx"], order_by="parent asc, idx asc"
)
ordered_attribute_value_map = frappe._dict()
for iv in item_attribute_values:
ordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value)
@@ -93,18 +97,18 @@ def get_attributes_and_values(item_code):
for attr in attributes:
valid_attribute_values = valid_options.get(attr.attribute, [])
ordered_values = ordered_attribute_value_map.get(attr.attribute, [])
attr['values'] = [v for v in ordered_values if v in valid_attribute_values]
attr["values"] = [v for v in ordered_values if v in valid_attribute_values]
return attributes
@frappe.whitelist(allow_guest=True)
def get_next_attribute_and_values(item_code, selected_attributes):
'''Find the count of Items that match the selected attributes.
"""Find the count of Items that match the selected attributes.
Also, find the attribute values that are not applicable for further searching.
If less than equal to 10 items are found, return item_codes of those items.
If one item is matched exactly, return item_code of that item.
'''
"""
selected_attributes = frappe.parse_json(selected_attributes)
item_cache = ItemVariantsCacheManager(item_code)
@@ -133,7 +137,11 @@ def get_next_attribute_and_values(item_code, selected_attributes):
for row in item_variants_data:
item_code, attribute, attribute_value = row
if item_code in filtered_items and attribute not in selected_attributes and attribute in attribute_list:
if (
item_code in filtered_items
and attribute not in selected_attributes
and attribute in attribute_list
):
valid_options_for_attributes[attribute].add(attribute_value)
optional_attributes = item_cache.get_optional_attributes()
@@ -159,12 +167,12 @@ def get_next_attribute_and_values(item_code, selected_attributes):
product_info = None
return {
'next_attribute': next_attribute,
'valid_options_for_attributes': valid_options_for_attributes,
'filtered_items_count': filtered_items_count,
'filtered_items': filtered_items if filtered_items_count < 10 else [],
'exact_match': exact_match,
'product_info': product_info
"next_attribute": next_attribute,
"valid_options_for_attributes": valid_options_for_attributes,
"filtered_items_count": filtered_items_count,
"filtered_items": filtered_items if filtered_items_count < 10 else [],
"exact_match": exact_match,
"product_info": product_info,
}
@@ -179,16 +187,16 @@ def get_items_with_selected_attributes(item_code, selected_attributes):
return set.intersection(*items)
# utilities
def get_item_attributes(item_code):
attributes = frappe.db.get_all('Item Variant Attribute',
fields=['attribute'],
filters={
'parenttype': 'Item',
'parent': item_code
},
order_by='idx asc'
attributes = frappe.db.get_all(
"Item Variant Attribute",
fields=["attribute"],
filters={"parenttype": "Item", "parent": item_code},
order_by="idx asc",
)
optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes()
@@ -199,6 +207,7 @@ def get_item_attributes(item_code):
return attributes
def get_item_variant_price_dict(item_code, cart_settings):
if cart_settings.enabled and cart_settings.show_price:
is_guest = frappe.session.user == "Guest"
@@ -207,12 +216,8 @@ def get_item_variant_price_dict(item_code, cart_settings):
if not is_guest or not cart_settings.hide_price_for_guest:
price_list = _set_price_list(cart_settings, None)
price = get_price(
item_code,
price_list,
cart_settings.default_customer_group,
cart_settings.company
item_code, price_list, cart_settings.default_customer_group, cart_settings.company
)
return {"price": price}
return None