fix: Discount Filters and Web templates

- Fixed discount filters (didn’t work after js render change)
- Fix Item Card Group template height and style
- Add placeholder to missing images in Product Category Cards template
- Code cleanup
This commit is contained in:
marination 2021-05-25 01:35:22 +05:30
parent 03b88bd7b8
commit b29c5d6e84
15 changed files with 215 additions and 204 deletions

View File

@ -115,7 +115,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
next_attribute = attribute
break
valid_options_for_attributes = frappe._dict({})
valid_options_for_attributes = frappe._dict()
for a in attribute_list:
valid_options_for_attributes[a] = set()

View File

@ -12,6 +12,7 @@ erpnext.ProductGrid = class {
this.products_section.addClass("hidden");
}
this.products_section.empty();
this.make();
}

View File

@ -12,6 +12,7 @@ erpnext.ProductList = class {
this.products_section.addClass("hidden");
}
this.products_section.empty();
this.make();
}

View File

@ -106,7 +106,7 @@ class ProductQuery:
def query_items(self, conditions, or_conditions, substitutions, start=0, with_attributes=False):
"""Build a query to fetch Website Items based on field filters."""
self.query_fields = (", ").join(self.fields)
self.query_fields = ", ".join(self.fields)
attribute_table = ", `tabItem Variant Attribute` iva" if with_attributes else ""
@ -119,9 +119,9 @@ class ProductQuery:
{conditions}
{or_conditions}
limit {self.page_length} offset {start}
""",
tuple(substitutions),
as_dict=1)
""",
tuple(substitutions),
as_dict=1)
def query_items_with_attributes(self, attributes, start=0):
"""Build a query to fetch Website Items based on field & attribute filters."""
@ -147,7 +147,7 @@ class ProductQuery:
all_items.append(set(items_dict.keys()))
result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
result = [items_dict.get(item) for item in set.intersection(*all_items)]
return result
def build_fields_filters(self, filters):
@ -192,11 +192,8 @@ class ProductQuery:
# Join the meta fields and default fields set
search_fields = default_fields.union(meta_fields)
try:
if frappe.db.count('Item', cache=True) > 50000:
search_fields.remove('description')
except KeyError:
pass
if frappe.db.count('Item', cache=True) > 50000:
search_fields.discard('description')
# Build or filters for query
search = '%{}%'.format(search_term)

View File

@ -6,8 +6,11 @@ erpnext.ProductView = class {
*/
constructor(options) {
Object.assign(this, options);
this.preference = "List View";
this.preference = this.view_type;
this.make();
}
make() {
this.products_section.empty();
this.prepare_view_toggler();
this.get_item_filter_data();
@ -22,12 +25,12 @@ erpnext.ProductView = class {
}
get_item_filter_data() {
// Get and render all Items related components
// Get and render all Product related views
let me = this;
let args = this.get_query_filters();
$('#list').prop('disabled', true);
$('#image-view').prop('disabled', true);
this.disable_view_toggler(true);
frappe.call({
method: 'erpnext.e_commerce.doctype.website_item.website_item.get_product_filter_data',
args: args,
@ -55,14 +58,20 @@ erpnext.ProductView = class {
me.render_no_products_section();
}
$('#list').prop('disabled', false);
$('#image-view').prop('disabled', false);
me.disable_view_toggler(false);
}
});
}
disable_view_toggler(disable=false) {
$('#list').prop('disabled', disable);
$('#image-view').prop('disabled', disable);
}
render_filters(filter_data) {
this.get_discount_filter_html(filter_data.discount_filters);
this.bind_filters();
this.restore_filters_state();
}
render_grid_view(items, settings) {
@ -226,9 +235,11 @@ erpnext.ProductView = class {
html += `
<div class="checkbox">
<label data-value="${ filter[0] }">
<input type="radio" class="product-filter discount-filter"
<input type="radio"
class="product-filter discount-filter"
name="discount" id="${ filter[0] }"
data-filter-name="discount" data-filter-value="${ filter[0] }"
data-filter-name="discount"
data-filter-value="${ filter[0] }"
>
<span class="label-area" for="${ filter[0] }">
${ filter[1] }
@ -243,6 +254,97 @@ erpnext.ProductView = class {
}
}
bind_filters() {
let me = this;
this.field_filters = {};
this.attribute_filters = {};
$('.product-filter').on('change', (e) => {
const $checkbox = $(e.target);
const is_checked = $checkbox.is(':checked');
if ($checkbox.is('.attribute-filter')) {
const {
attributeName: attribute_name,
attributeValue: attribute_value
} = $checkbox.data();
if (is_checked) {
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
this.attribute_filters[attribute_name].push(attribute_value);
} else {
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name].filter(v => v !== attribute_value);
}
if (this.attribute_filters[attribute_name].length === 0) {
delete this.attribute_filters[attribute_name];
}
} else if ($checkbox.is('.field-filter') || $checkbox.is('.discount-filter')) {
const {
filterName: filter_name,
filterValue: filter_value
} = $checkbox.data();
if ($checkbox.is('.discount-filter')) {
// clear previous discount filter to accomodate new
delete this.field_filters["discount"];
}
if (is_checked) {
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
this.field_filters[filter_name].push(filter_value);
} else {
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
this.field_filters[filter_name] = this.field_filters[filter_name].filter(v => v !== filter_value);
}
if (this.field_filters[filter_name].length === 0) {
delete this.field_filters[filter_name];
}
}
let route_params = frappe.utils.get_query_params();
const query_string = get_query_string({
start: if_key_exists(route_params.start) || 0,
field_filters: JSON.stringify(if_key_exists(this.field_filters)),
attribute_filters: JSON.stringify(if_key_exists(this.attribute_filters)),
});
window.history.pushState('filters', '', `${location.pathname}?` + query_string);
$('.page_content input').prop('disabled', true);
me.make();
$('.page_content input').prop('disabled', false);
});
}
restore_filters_state() {
const filters = frappe.utils.get_query_params();
let {field_filters, attribute_filters} = filters;
if (field_filters) {
field_filters = JSON.parse(field_filters);
for (let fieldname in field_filters) {
const values = field_filters[fieldname];
const selector = values.map(value => {
return `input[data-filter-name="${fieldname}"][data-filter-value="${value}"]`;
}).join(',');
$(selector).prop('checked', true);
}
this.field_filters = field_filters;
}
if (attribute_filters) {
attribute_filters = JSON.parse(attribute_filters);
for (let attribute in attribute_filters) {
const values = attribute_filters[attribute];
const selector = values.map(value => {
return `input[data-attribute-name="${attribute}"][data-attribute-value="${value}"]`;
}).join(',');
$(selector).prop('checked', true);
}
this.attribute_filters = attribute_filters;
}
}
render_no_products_section() {
this.products_section.append(`
<br><br><br>
@ -278,4 +380,26 @@ erpnext.ProductView = class {
$("#product-listing").prepend(sub_group_html);
}
}
};
};
function get_query_string(object) {
const url = new URLSearchParams();
for (let key in object) {
const value = object[key];
if (value) {
url.append(key, value);
}
}
return url.toString();
}
function if_key_exists(obj) {
let exists = false;
for (let key in obj) {
if (obj.hasOwnProperty(key) && obj[key]) {
exists = true;
break;
}
}
return exists ? obj : undefined;
}

View File

@ -6,8 +6,15 @@
}) -%}
<div class="card h-100">
{% if image %}
<img class="card-img-top" src="{{ image }}" alt="{{ title }}">
<img class="card-img-top" src="{{ image }}" alt="{{ title }}" style="max-height: 200px;">
{% else %}
<div class="placeholder-div" style="max-height: 200px;">
<span class="placeholder">
{{ frappe.utils.get_abbr(title or '') }}
</span>
</div>
{% endif %}
<div class="card-body text-center text-muted small">
{{ title or '' }}
</div>

View File

@ -54,12 +54,12 @@ def execute():
for doctype in ("Website Item Group", "Item Website Specification"):
web_item, item = website_item.name, item.item_code
frappe.db.sql(f"""
Update `tab{doctype}`
Update
`tab{doctype}`
set
parenttype = 'Website Item',
parent = '{web_item}'
where
parenttype = 'Item'
and parent = '{item}'
"""
)
""")

View File

@ -84,8 +84,8 @@ $.extend(wishlist, {
const $wish_icon = $btn.find('.wish-icon');
let me = this;
if(frappe.session.user==="Guest") {
if(localStorage) {
if (frappe.session.user==="Guest") {
if (localStorage) {
localStorage.setItem("last_visited", window.location.pathname);
}
window.location.href = "/login";
@ -137,7 +137,7 @@ $.extend(wishlist, {
failure_action: method to execute on failure,
async: make call asynchronously (true/false). */
if (frappe.session.user==="Guest") {
if(localStorage) {
if (localStorage) {
localStorage.setItem("last_visited", window.location.pathname);
}
window.location.href = "/login";

View File

@ -782,3 +782,16 @@ body.product-page {
#toggle-view {
float: right;
}
.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;
}

View File

@ -73,10 +73,10 @@
{% if is_featured %}
<div class="col-sm-{{ col_size*2 }} item-card">
<div class="card featured-item {{ align_items_class }}">
<div class="card featured-item {{ align_items_class }}" style="height: 360px;">
{% if image %}
<div class="row no-gutters">
<div class="col-md-6">
<div class="col-md-5 ml-4">
<img class="card-img" src="{{ image }}" alt="{{ title }}">
</div>
<div class="col-md-6">
@ -92,7 +92,7 @@
</div>
{% else %}
<div class="col-sm-{{ col_size }} item-card">
<div class="card {{ align_items_class }}">
<div class="card {{ align_items_class }}" style="height: 360px;">
{% if image %}
<div class="card-img-container">
<a href="/{{ item.route or '#' }}" style="text-decoration: none;">
@ -119,19 +119,17 @@
'text-left': align == 'Left' or is_featured,
}) -%}
<div class="card-body {{ align_class }}" style="width:100%">
<div style="margin-top: 16px; display: flex;">
<div class="mt-4">
<a href="/{{ item.route or '#' }}">
<div class="product-title">
{{ title or '' }}
{% if item.in_stock %}
<span class="indicator {{ item.in_stock }} card-indicator"></span>
{% endif %}
</div>
</a>
</div>
{% if is_featured %}
<div class="product-price">{{ item.formatted_price or '' }}</div>
<div class="product-description ellipsis">{{ description or '' }}</div>
<div class="product-description ellipsis text-muted" style="white-space: normal;">
{{ description or '' }}
</div>
{% else %}
<div class="product-category">{{ item.item_group or '' }}</div>
{% endif %}

View File

@ -1,15 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
no_cache = 1
import frappe
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
def get_context(context):
context.no_cache = 1
context.full_page = True
context.reviews = None
if frappe.form_dict and frappe.form_dict.get("item_code"):
context.item_code = frappe.form_dict.get("item_code")
context.web_item = frappe.db.get_value("Website Item", {"item_code": context.item_code}, "name")

View File

@ -1,9 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
no_cache = 1
import frappe
from erpnext.utilities.product import get_price
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
@ -32,6 +28,7 @@ def get_context(context):
context.items = items
context.settings = settings
context.no_cache = 1
def get_stock_availability(item_code, warehouse):
stock_qty = frappe.utils.flt(
@ -42,7 +39,7 @@ def get_stock_availability(item_code, warehouse):
},
"actual_qty")
)
return True if stock_qty else False
return bool(stock_qty)
def get_wishlist_items():
if frappe.db.exists("Wishlist", frappe.session.user):
@ -53,5 +50,5 @@ def get_wishlist_items():
from
`tabWishlist Items`
where
parent=%(user)s""" % {"user": frappe.db.escape(frappe.session.user)}, as_dict=1)
parent=%(user)s""", {"user": frappe.session.user}, as_dict=1)
return

View File

@ -5,86 +5,18 @@ $(() => {
let is_item_group_page = $(".item-group-content").data("item-group");
this.item_group = is_item_group_page || null;
// Render Products and Discount Filters
let view_type = "List View";
// Render Product Views and setup Filters
frappe.require('assets/js/e-commerce.min.js', function() {
new erpnext.ProductView({
view_type: "List",
view_type: view_type,
products_section: $('#product-listing'),
item_group: me.item_group
});
});
this.bind_filters();
this.bind_card_actions();
this.bind_search();
this.restore_filters_state();
}
bind_filters() {
let me = this;
this.field_filters = {};
this.attribute_filters = {};
$('.product-filter').on('change', frappe.utils.debounce((e) => {
const $checkbox = $(e.target);
const is_checked = $checkbox.is(':checked');
if ($checkbox.is('.attribute-filter')) {
const {
attributeName: attribute_name,
attributeValue: attribute_value
} = $checkbox.data();
if (is_checked) {
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
this.attribute_filters[attribute_name].push(attribute_value);
} else {
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name].filter(v => v !== attribute_value);
}
if (this.attribute_filters[attribute_name].length === 0) {
delete this.attribute_filters[attribute_name];
}
} else if ($checkbox.is('.field-filter') || $checkbox.is('.discount-filter')) {
const {
filterName: filter_name,
filterValue: filter_value
} = $checkbox.data();
if ($checkbox.is('.discount-filter')) {
// clear previous discount filter to accomodate new
delete this.field_filters["discount"];
}
if (is_checked) {
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
this.field_filters[filter_name].push(filter_value);
} else {
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
this.field_filters[filter_name] = this.field_filters[filter_name].filter(v => v !== filter_value);
}
if (this.field_filters[filter_name].length === 0) {
delete this.field_filters[filter_name];
}
}
const query_string = get_query_string({
field_filters: JSON.stringify(if_key_exists(this.field_filters)),
attribute_filters: JSON.stringify(if_key_exists(this.attribute_filters)),
});
window.history.pushState('filters', '', `${location.pathname}?` + query_string);
$('.page_content input').prop('disabled', true);
frappe.require('assets/js/e-commerce.min.js', function() {
new erpnext.ProductView({
view_type: "List",
products_section: $('#product-listing'),
item_group: me.item_group
});
$('.page_content input').prop('disabled', false);
});
}, 1000));
}
bind_card_actions() {
@ -92,70 +24,20 @@ $(() => {
e_commerce.wishlist.bind_wishlist_action();
}
bind_search() {
$('input[type=search]').on('keydown', (e) => {
if (e.keyCode === 13) {
// Enter
const value = e.target.value;
if (value) {
window.location.search = 'search=' + e.target.value;
} else {
window.location.search = '';
}
}
});
}
restore_filters_state() {
const filters = frappe.utils.get_query_params();
let {field_filters, attribute_filters} = filters;
if (field_filters) {
field_filters = JSON.parse(field_filters);
for (let fieldname in field_filters) {
const values = field_filters[fieldname];
const selector = values.map(value => {
return `input[data-filter-name="${fieldname}"][data-filter-value="${value}"]`;
}).join(',');
$(selector).prop('checked', true);
}
this.field_filters = field_filters;
}
if (attribute_filters) {
attribute_filters = JSON.parse(attribute_filters);
for (let attribute in attribute_filters) {
const values = attribute_filters[attribute];
const selector = values.map(value => {
return `input[data-attribute-name="${attribute}"][data-attribute-value="${value}"]`;
}).join(',');
$(selector).prop('checked', true);
}
this.attribute_filters = attribute_filters;
}
}
// bind_search() {
// $('input[type=search]').on('keydown', (e) => {
// if (e.keyCode === 13) {
// // Enter
// const value = e.target.value;
// if (value) {
// window.location.search = 'search=' + e.target.value;
// } else {
// window.location.search = '';
// }
// }
// });
// }
}
new ProductListing();
function get_query_string(object) {
const url = new URLSearchParams();
for (let key in object) {
const value = object[key];
if (value) {
url.append(key, value);
}
}
return url.toString();
}
function if_key_exists(obj) {
let exists = false;
for (let key in obj) {
if (obj.hasOwnProperty(key) && obj[key]) {
exists = true;
break;
}
}
return exists ? obj : undefined;
}
});

View File

@ -11,18 +11,6 @@
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 %}

View File

@ -4,7 +4,7 @@ from frappe import _
sitemap = 1
def get_context(context):
settings = frappe.get_doc("E Commerce Settings")
settings = frappe.get_cached_doc("E Commerce Settings")
context.categories_enabled = settings.enable_field_filters
if context.categories_enabled:
@ -23,9 +23,9 @@ def get_slideshow(slideshow):
'rounded': 1,
'slider_name': "Categories"
}
slideshow = frappe.get_doc("Website Slideshow", slideshow)
slideshow = frappe.get_cached_doc("Website Slideshow", slideshow)
slides = slideshow.get({"doctype": "Website Slideshow Item"})
for index, slide in enumerate(slides):
for index, slide in enumerate(slides, start=1):
values[f"slide_{index + 1}_image"] = slide.image
values[f"slide_{index + 1}_title"] = slide.heading
values[f"slide_{index + 1}_subtitle"] = slide.description
@ -41,7 +41,7 @@ def get_tabs(categories):
}
categorical_data = get_category_records(categories)
for index, tab in enumerate(categorical_data):
for index, tab in enumerate(categorical_data, start=1):
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(
@ -55,19 +55,24 @@ def get_category_records(categories):
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)
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)
categorical_data[category] = frappe.db.sql(f"""
Select {",".join(fields)}
from `tab{doctype}`""", as_dict=1)
return categorical_data