diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index b1b1cae770..75f9c31ce7 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -38,7 +38,9 @@ "enable_field_filters", "filter_fields", "enable_attribute_filters", - "filter_attributes" + "filter_attributes", + "shop_by_category_section", + "slideshow" ], "fields": [ { @@ -64,7 +66,7 @@ "collapsible": 1, "fieldname": "filter_categories_section", "fieldtype": "Section Break", - "label": "Filters" + "label": "Filters and Categories" }, { "default": "0", @@ -78,9 +80,10 @@ }, { "default": "0", + "description": "The field filters will also work as categories in the Shop by Category page.", "fieldname": "enable_field_filters", "fieldtype": "Check", - "label": "Enable Field Filters" + "label": "Enable Field Filters (Categories)" }, { "default": "0", @@ -258,12 +261,25 @@ "label": "Payment Gateway Account", "mandatory_depends_on": "enable_checkout", "options": "Payment Gateway Account" + }, + { + "collapsible": 1, + "depends_on": "enable_field_filters", + "fieldname": "shop_by_category_section", + "fieldtype": "Section Break", + "label": "Shop by Category" + }, + { + "fieldname": "slideshow", + "fieldtype": "Link", + "label": "Slideshow", + "options": "Website Slideshow" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-02-11 18:22:14.556880", + "modified": "2021-03-01 20:24:56.548673", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index e3197b9fb0..856b9b7508 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -165,7 +165,7 @@ class WebsiteItem(WebsiteGenerator): context.show_search = True context.search_link = '/search' - context.parents = get_parent_item_groups(self.item_group) + context.parents = get_parent_item_groups(self.item_group, from_item=True) context.body_class = "product-page" self.attributes = frappe.get_all("Item Variant Attribute", fields=["attribute", "attribute_value"], diff --git a/erpnext/e_commerce/product_configurator/utils.py b/erpnext/e_commerce/product_configurator/utils.py index 6c652cf457..7ccb053adb 100644 --- a/erpnext/e_commerce/product_configurator/utils.py +++ b/erpnext/e_commerce/product_configurator/utils.py @@ -2,7 +2,6 @@ import frappe from frappe.utils import cint from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager -from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website def get_item_codes_by_attributes(attribute_filters, template_item_code=None): items = [] diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index fef1e76154..da22aa6e4d 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -323,6 +323,32 @@ body.product-page { } } +.sub-category-container { + padding-bottom: 1rem; + margin-bottom: 1.25rem; + border-bottom: 1px solid var(--table-border-color); + + .heading { + color: var(--gray-500); + } +} + +.scroll-categories { + white-space: nowrap; + overflow-x: auto; + + .category-pill { + margin: 0px 4px; + display: inline-block; + padding: 6px 12px; + background-color: #ecf5fe; + width: fit-content; + font-size: 14px; + border-radius: 18px; + color: var(--blue-500); + } +} + .cart-icon { .cart-badge { position: relative; diff --git a/erpnext/setup/doctype/brand/brand.json b/erpnext/setup/doctype/brand/brand.json index a8f0674b1f..45b4db81f1 100644 --- a/erpnext/setup/doctype/brand/brand.json +++ b/erpnext/setup/doctype/brand/brand.json @@ -1,270 +1,111 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:brand", - "beta": 0, - "creation": "2013-02-22 01:27:54", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:brand", + "creation": "2013-02-22 01:27:54", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "brand", + "image", + "description", + "defaults", + "brand_defaults" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 1, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "brand", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Brand Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "brand", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_in_quick_entry": 1, + "fieldname": "brand", + "fieldtype": "Data", + "label": "Brand Name", + "oldfieldname": "brand", + "oldfieldtype": "Data", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "description", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "defaults", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Defaults", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "defaults", + "fieldtype": "Section Break", + "label": "Defaults" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "brand_defaults", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Brand Defaults", - "length": 0, - "no_copy": 0, - "options": "Item Default", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "brand_defaults", + "fieldtype": "Table", + "label": "Brand Defaults", + "options": "Item Default" + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-certificate", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-10-23 23:18:06.067612", - "modified_by": "Administrator", - "module": "Setup", - "name": "Brand", - "owner": "Administrator", + ], + "icon": "fa fa-certificate", + "idx": 1, + "image_field": "image", + "links": [], + "modified": "2021-03-01 15:57:30.005783", + "modified_by": "Administrator", + "module": "Setup", + "name": "Brand", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Item Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Item Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User" } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} + ], + "quick_entry": 1, + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "ASC" +} \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 1e942d7b47..b375711b52 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -88,8 +88,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): if not field_filters: field_filters = {} - # Ensure the query remains within current item group & sub group - field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)] + # Ensure the query remains within current item group + field_filters['item_group'] = self.name engine = ProductQuery() context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) @@ -104,6 +104,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): "title": self.name }) + context.sub_categories = get_child_groups(self.name) if self.slideshow: values = { 'show_indicators': 1, @@ -123,8 +124,9 @@ class ItemGroup(NestedSet, WebsiteGenerator): context.slideshow = values - context.breadcrumbs = 0 + context.no_breadcrumbs = False context.title = self.website_title or self.name + context.body_class = "product-page" return context @@ -136,10 +138,11 @@ class ItemGroup(NestedSet, WebsiteGenerator): validate_item_default_company_links(self.item_group_defaults) def get_child_groups(item_group_name): + """Returns child item groups *excluding* passed group.""" item_group = frappe.get_doc("Item Group", item_group_name) - return frappe.db.sql("""select name - from `tabItem Group` where lft>=%(lft)s and rgt<=%(rgt)s - and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt}) + return frappe.db.sql("""select name, route + from `tabItem Group` where lft>%(lft)s and rgt<%(rgt)s + and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt}, as_dict=1) def get_child_item_groups(item_group_name): item_group = frappe.get_cached_value("Item Group", @@ -164,15 +167,25 @@ def get_item_for_list_in_html(context): return frappe.get_template(products_template).render(context) -def get_parent_item_groups(item_group_name): +def get_parent_item_groups(item_group_name, from_item=False): + base_nav_page = {"name": frappe._("Shop by Category"), "route":"/shop-by-category"} + + if from_item: + # base page after 'Home' will vary on Item page + last_page = frappe.request.environ["HTTP_REFERER"].split('/')[-1] + if last_page and last_page in ("shop-by-category", "all-products"): + base_nav_page_title = " ".join(last_page.split("-")).title() + base_nav_page = {"name": frappe._(base_nav_page_title), "route":"/"+last_page} + base_parents = [ {"name": frappe._("Home"), "route":"/"}, - {"name": frappe._("All Products"), "route":"/all-products"}, + base_nav_page, ] + if not item_group_name: return base_parents - item_group = frappe.get_doc("Item Group", item_group_name) + item_group = frappe.db.get_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) parent_groups = frappe.db.sql("""select name, route from `tabItem Group` where lft <= %s and rgt >= %s and show_in_website=1 diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index b5f18ba66d..233b16974b 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -8,6 +8,12 @@ {% endblock %} +{% block breadcrumbs %} +
+ {% include "templates/includes/breadcrumbs.html" %} +
+{% endblock %} + {% block page_content %}
@@ -27,6 +33,20 @@
+ {% if sub_categories %} +
+
{{ _('Sub Categories') }}
+
+
+ {% for row in sub_categories%} + +
+ {{ row.name }} +
+
+ {% endfor %} +
+ {% endif %}
{% if items %} {% for item in items %} diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/www/shop-by-category/category_card_section.html b/erpnext/www/shop-by-category/category_card_section.html new file mode 100644 index 0000000000..532a198661 --- /dev/null +++ b/erpnext/www/shop-by-category/category_card_section.html @@ -0,0 +1,28 @@ +{%- macro card(title, image, type, url=None, text_primary=False) -%} + +
+ {% if image %} + {{ title }} + {% else %} +
+ AB +
+ {% endif %} +
+ {{ title or '' }} +
+ +
+{%- endmacro -%} + +
+
+ {%- for row in data -%} + {%- set title = row.name -%} + {%- set image = row.get("image") -%} + {%- if title -%} + {{ card(title, image, type, row.get("route")) }} + {%- endif -%} + {%- endfor -%} +
+
\ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.html b/erpnext/www/shop-by-category/index.html new file mode 100644 index 0000000000..ac0b317665 --- /dev/null +++ b/erpnext/www/shop-by-category/index.html @@ -0,0 +1,60 @@ +{% extends "templates/web.html" %} +{% block title %}{{ _('Shop by Category') }}{% endblock %} + +{% block head_include %} + +{% endblock %} + +{% block script %} + +{% endblock %} + +{% block page_content %} +
+
+ {% if slideshow %} + + {{ web_block( + "Hero Slider", + values=slideshow, + add_container=0, + add_top_padding=0, + add_bottom_padding=0, + ) }} + {% endif %} +
+
+ {% if tabs %} + + {{ web_block( + "Section with Tabs", + values=tabs, + add_container=0, + add_top_padding=0, + add_bottom_padding=0 + ) }} + {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.js b/erpnext/www/shop-by-category/index.js new file mode 100644 index 0000000000..1b3116f5ba --- /dev/null +++ b/erpnext/www/shop-by-category/index.js @@ -0,0 +1,12 @@ +$(() => { + $('.category-card').on('click', (e) => { + let category_type = e.currentTarget.dataset.type; + let category_name = e.currentTarget.dataset.name; + + if (category_type != "item_group") { + let filters = {}; + filters[category_type] = [category_name]; + window.location.href = "/all-products?field_filters=" + JSON.stringify(filters); + } + }); +}); \ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py new file mode 100644 index 0000000000..c295335cdf --- /dev/null +++ b/erpnext/www/shop-by-category/index.py @@ -0,0 +1,73 @@ +import frappe +from frappe import _ + +sitemap = 1 + +def get_context(context): + settings = frappe.get_doc("E Commerce Settings") + context.categories_enabled = settings.enable_field_filters + + if context.categories_enabled: + categories = [row.fieldname for row in settings.filter_fields] + context.tabs = get_tabs(categories) + + if settings.slideshow: + context.slideshow = get_slideshow(settings.slideshow) + + context.no_cache = 1 + +def get_slideshow(slideshow): + values = { + 'show_indicators': 1, + 'show_controls': 1, + 'rounded': 1, + 'slider_name': "Categories" + } + slideshow = frappe.get_doc("Website Slideshow", slideshow) + slides = slideshow.get({"doctype": "Website Slideshow Item"}) + for index, slide in enumerate(slides): + values[f"slide_{index + 1}_image"] = slide.image + values[f"slide_{index + 1}_title"] = slide.heading + values[f"slide_{index + 1}_subtitle"] = slide.description + values[f"slide_{index + 1}_theme"] = slide.get("theme") or "Light" + values[f"slide_{index + 1}_content_align"] = slide.get("content_align") or "Centre" + values[f"slide_{index + 1}_primary_action"] = slide.url + + return values + +def get_tabs(categories): + tab_values = { + 'title': _("Shop by Category"), + } + + categorical_data = get_category_records(categories) + for index, tab in enumerate(categorical_data): + tab_values[f"tab_{index + 1}_title"] = frappe.unscrub(tab) + # pre-render cards for each tab + tab_values[f"tab_{index + 1}_content"] = frappe.render_template( + "erpnext/www/shop-by-category/category_card_section.html", + {"data": categorical_data[tab], "type": tab} + ) + return tab_values + +def get_category_records(categories): + categorical_data = {} + for category in categories: + if category == "item_group": + categorical_data["item_group"] = frappe.db.sql(""" + Select name, parent_item_group, is_group, image, route + from `tabItem Group` + where parent_item_group='All Item Groups' + and show_in_website=1""", as_dict=1) + else: + doctype = frappe.unscrub(category) + fields = ["name"] + if frappe.get_meta(doctype, cached=True).get_field("image"): + fields += ["image"] + + categorical_data[category] = frappe.db.sql(""" + Select {fields} + from `tab{doctype}`""".format(doctype=doctype, fields=",".join(fields)), as_dict=1) + + return categorical_data +