feat: Shop by Category

- Added Shop by Category Page
- Tabbed sections for item fields in Shop by Category Page
- Added Shop by Category Section in E commerce Settings
- Nested Navigation & Breadcrumbs in Item group pages
- Added scrollable & clickable Sub categories in Item Group page
- Made breadcrumbs slightly dynamic in Item Page
- Added image to Brand doctype
This commit is contained in:
marination 2021-03-02 19:54:01 +05:30
parent 22f41a17b7
commit d7130e31fe
12 changed files with 356 additions and 268 deletions

View File

@ -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 <b>Shop by Category</b> 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",

View File

@ -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"],

View File

@ -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 = []

View File

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

View File

@ -1,270 +1,111 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"actions": [],
"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,
"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,
"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,
"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
"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
"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",
"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,
"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
"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
"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
"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
"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
"sort_field": "modified",
"sort_order": "ASC"
}

View File

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

View File

@ -8,6 +8,12 @@
<script type="text/javascript" src="/all-products/index.js"></script>
{% endblock %}
{% block breadcrumbs %}
<div class="item-breadcrumbs small text-muted">
{% include "templates/includes/breadcrumbs.html" %}
</div>
{% endblock %}
{% block page_content %}
<div class="item-group-content" itemscope itemtype="http://schema.org/Product" data-item-group="{{ name }}">
<div class="item-group-slideshow">
@ -27,6 +33,20 @@
</div>
<div class="row">
<div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
{% if sub_categories %}
<div class="sub-category-container">
<div class="heading"> {{ _('Sub Categories') }} </div>
</div>
<div class="sub-category-container scroll-categories">
{% for row in sub_categories%}
<a href="{{ row.route or '#' }}" style="text-decoration: none;">
<div class="category-pill">
{{ row.name }}
</div>
</a>
{% endfor %}
</div>
{% endif %}
<div class="row products-list">
{% if items %}
{% for item in items %}

View File

View File

@ -0,0 +1,28 @@
{%- macro card(title, image, type, url=None, text_primary=False) -%}
<!-- style defined at shop-by-category index -->
<div class="card category-card" data-type="{{ type }}" data-name="{{ title }}">
{% if image %}
<img class="card-img-top" src="{{ image }}" alt="{{ title }}" style="height:80%">
{% else %}
<div class="placeholder-div">
<span class="placeholder">AB</span>
</div>
{% endif %}
<div class="card-body text-center text-muted">
{{ title or '' }}
</div>
<a href="{{ url or '#' }}" class="stretched-link"></a>
</div>
{%- endmacro -%}
<div class="col-12 item-card-group-section">
<div class="row products-list product-category-section">
{%- for row in data -%}
{%- set title = row.name -%}
{%- set image = row.get("image") -%}
{%- if title -%}
{{ card(title, image, type, row.get("route")) }}
{%- endif -%}
{%- endfor -%}
</div>
</div>

View File

@ -0,0 +1,60 @@
{% extends "templates/web.html" %}
{% block title %}{{ _('Shop by Category') }}{% endblock %}
{% block head_include %}
<style>
.category-slideshow {
margin-bottom: 2rem;
}
.category-card {
height: 300px !important;
width: 300px !important;
margin: 30px !important;
}
.placeholder-div {
height:80%;
width: -webkit-fill-available;
padding: 50px;
text-align: center;
background-color: #F9FAFA;
border-top-left-radius: calc(0.75rem - 1px);
border-top-right-radius: calc(0.75rem - 1px);
}
.placeholder {
font-size: 72px;
}
</style>
{% endblock %}
{% block script %}
<script type="text/javascript" src="/shop-by-category/index.js"></script>
{% endblock %}
{% block page_content %}
<div class="shop-by-category-content">
<div class="category-slideshow">
{% if slideshow %}
<!-- slideshow -->
{{ web_block(
"Hero Slider",
values=slideshow,
add_container=0,
add_top_padding=0,
add_bottom_padding=0,
) }}
{% endif %}
</div>
<div class="category-tabs">
{% if tabs %}
<!-- tabs -->
{{ web_block(
"Section with Tabs",
values=tabs,
add_container=0,
add_top_padding=0,
add_bottom_padding=0
) }}
{% endif %}
</div>
</div>
{% endblock %}

View File

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

View File

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