feat: Discount Filters
- Discount filters in filters section
- Code cleanup
diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py
index 637194e..f19dcf3 100644
--- a/erpnext/e_commerce/doctype/item_review/item_review.py
+++ b/erpnext/e_commerce/doctype/item_review/item_review.py
@@ -75,4 +75,4 @@
if customer:
return frappe.db.get_value("Customer", customer)
else:
- frappe.throw("You are not verified to write a review yet. Please contact us for verification.")
\ No newline at end of file
+ frappe.throw(_("You are not verified to write a review yet. Please contact us for verification."))
\ No newline at end of file
diff --git a/erpnext/e_commerce/filters.py b/erpnext/e_commerce/filters.py
index 3ca3d26..9ad817c 100644
--- a/erpnext/e_commerce/filters.py
+++ b/erpnext/e_commerce/filters.py
@@ -2,7 +2,8 @@
# License: GNU General Public License v3. See license.txt
import frappe
-
+from frappe import _dict
+from frappe.utils import floor, ceil, flt
class ProductFiltersBuilder:
def __init__(self, item_group=None):
@@ -88,3 +89,19 @@
for name, values in attribute_value_map.items():
out.append(frappe._dict(name=name, item_attribute_values=values))
return out
+
+ def get_discount_filters(self, discounts):
+ discount_filters = []
+
+ # [25.89, 60.5]
+ min_discount, max_discount = discounts[0], discounts[1]
+ # [25, 60]
+ 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
+
+ for discount in range(min_range, (max_range + 1), 10):
+ label = f"{discount}% and above"
+ discount_filters.append([discount, label])
+
+ return discount_filters
diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py
index 2dbed0a..c186a05 100644
--- a/erpnext/e_commerce/product_query.py
+++ b/erpnext/e_commerce/product_query.py
@@ -3,6 +3,8 @@
import frappe
+from frappe.utils import flt
+
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
@@ -39,25 +41,15 @@
Returns:
list: List of results with set fields
"""
+ result, discount_list = [], []
+
if fields:
self.build_fields_filters(fields)
if search_term:
self.build_search_filters(search_term)
-
if self.settings.hide_variants:
self.conditions += " and wi.variant_of is null"
- result = []
- website_item_groups = []
-
- # if from item group page consider website item group table
- if item_group:
- website_item_groups = frappe.db.get_all(
- "Item",
- fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
- filters=[["Website Item Group", "item_group", "=", item_group]]
- )
-
if attributes:
result = self.query_items_with_attributes(attributes, start)
else:
@@ -67,33 +59,50 @@
# add price and availability info in results
for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
+
if product_info and product_info['price']:
- item.formatted_mrp = product_info['price'].get('formatted_mrp')
- item.formatted_price = product_info['price'].get('formatted_price')
- if item.formatted_mrp:
- item.discount = product_info['price'].get('formatted_discount_percent') or \
- product_info['price'].get('formatted_discount_rate')
- item.price = product_info['price'].get('price_list_rate')
+ self.get_price_discount_info(item, product_info['price'], discount_list)
if self.settings.show_stock_availability:
- if item.get("website_warehouse"):
- stock_qty = frappe.utils.flt(
- frappe.db.get_value("Bin",
- {
- "item_code": item.item_code,
- "warehouse": item.get("website_warehouse")
- },
- "actual_qty")
- )
- item.in_stock = "green" if stock_qty else "red"
- elif not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
- item.in_stock = "green" # non-stock item will always be available
+ self.get_stock_availability(item)
item.wished = False
if frappe.db.exists("Wishlist Items", {"item_code": item.item_code, "parent": frappe.session.user}):
item.wished = True
- return result
+ discounts = []
+ if discount_list:
+ discounts = [min(discount_list), max(discount_list)]
+
+ if fields and "discount" in fields:
+ 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]
+
+ return result, discounts
+
+ def get_price_discount_info(self, item, price_object, discount_list):
+ """Modify item object and add price details."""
+ item.formatted_mrp = price_object.get('formatted_mrp')
+ item.formatted_price = price_object.get('formatted_price')
+
+ 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.price = price_object.get('price_list_rate')
+
+ def get_stock_availability(self, item):
+ """Modify item object and add stock details."""
+ if item.get("website_warehouse"):
+ stock_qty = frappe.utils.flt(
+ frappe.db.get_value("Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")},
+ "actual_qty"))
+ item.in_stock = "green" if stock_qty else "red"
+ elif not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
+ item.in_stock = "green" # non-stock item will always be available
def query_items(self, conditions, or_conditions, substitutions, start=0):
"""Build a query to fetch Website Items based on field filters."""
@@ -150,7 +159,7 @@
filters (dict): Filters
"""
for field, values in filters.items():
- if not values:
+ if not values or field == "discount":
continue
# handle multiselect fields in filter addition
diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js
index b2c840a..6bcb6b1 100644
--- a/erpnext/public/js/wishlist.js
+++ b/erpnext/public/js/wishlist.js
@@ -48,7 +48,7 @@
});
let success_action = function() {
- const $card_wrapper = $move_to_cart_btn.closest(".item-card");
+ const $card_wrapper = $move_to_cart_btn.closest(".wishlist-card");
$card_wrapper.addClass("wish-removed");
};
let args = { item_code: item_code };
@@ -63,7 +63,7 @@
let item_code = $remove_wish_btn.data("item-code");
let success_action = function() {
- const $card_wrapper = $remove_wish_btn.closest(".item-card");
+ const $card_wrapper = $remove_wish_btn.closest(".wishlist-card");
$card_wrapper.addClass("wish-removed");
};
let args = { item_code: item_code };
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index cdff775..9ff9260 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -93,12 +93,14 @@
field_filters['item_group'] = self.name
engine = ProductQuery()
- context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
+ context.items, discounts = engine.query(attribute_filters, field_filters, search, start)
filter_engine = ProductFiltersBuilder(self.name)
context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_filters()
+ if discounts:
+ context.discount_filters = filter_engine.get_discount_filters(discounts)
context.update({
"parents": get_parent_item_groups(self.parent_item_group),
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index 233b169..a27b566 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -1,3 +1,4 @@
+{% from "erpnext/templates/includes/macros.html" import field_filter_section, attribute_filter_section, discount_range_filters %}
{% extends "templates/web.html" %}
{% block header %}
@@ -63,68 +64,16 @@
<div class="mb-4 filters-title" > {{ _('Filters') }} </div>
<a class="mb-4 clear-filters" href="/{{ doc.route }}">{{ _('Clear All') }}</a>
</div>
- {% for field_filter in field_filters %}
- {%- set item_field = field_filter[0] %}
- {%- set values = field_filter[1] %}
- <div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ item_field.label }}</div>
+ <!-- field filters -->
+ {{ field_filter_section(field_filters) }}
- {% if values | len > 20 %}
- <!-- show inline filter if values more than 20 -->
- <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
- {% endif %}
+ <!-- attribute filters -->
+ {{ attribute_filter_section(attribute_filters) }}
- {% if values %}
- <div class="filter-options">
- {% for value in values %}
- <div class="checkbox" data-value="{{ value }}">
- <label for="{{value}}">
- <input type="checkbox"
- class="product-filter field-filter"
- id="{{value}}"
- data-filter-name="{{ item_field.fieldname }}"
- data-filter-value="{{ value }}"
- >
- <span class="label-area">{{ value }}</span>
- </label>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <i class="text-muted">{{ _('No values') }}</i>
- {% endif %}
- </div>
- {% endfor %}
-
- {% for attribute in attribute_filters %}
- <div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ attribute.name}}</div>
- {% if values | len > 20 %}
- <!-- show inline filter if values more than 20 -->
- <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
- {% endif %}
-
- {% if attribute.item_attribute_values %}
- <div class="filter-options">
- {% for attr_value in attribute.item_attribute_values %}
- <div class="checkbox">
- <label data-value="{{ value }}">
- <input type="checkbox"
- class="product-filter attribute-filter"
- id="{{attr_value.name}}"
- data-attribute-name="{{ attribute.name }}"
- data-attribute-value="{{ attr_value.attribute_value }}"
- {% if attr_value.checked %} checked {% endif %}>
- <span class="label-area">{{ attr_value.attribute_value }}</span>
- </label>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <i class="text-muted">{{ _('No values') }}</i>
- {% endif %}
- </div>
- {% endfor %}
+ <!-- discount filters -->
+ {% if discount_filters %}
+ {{ discount_range_filters(discount_filters) }}
+ {% endif %}
</div>
<script>
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index 1063704..2c5f7b9 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -315,3 +315,91 @@
{% endfor %}
</div>
{%- endmacro -%}
+
+{%- macro field_filter_section(filters)-%}
+{% for field_filter in filters %}
+ {%- set item_field = field_filter[0] %}
+ {%- set values = field_filter[1] %}
+ <div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ item_field.label }}</div>
+
+ {% if values | len > 20 %}
+ <!-- show inline filter if values more than 20 -->
+ <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
+ {% endif %}
+
+ {% if values %}
+ <div class="filter-options">
+ {% for value in values %}
+ <div class="checkbox" data-value="{{ value }}">
+ <label for="{{value}}">
+ <input type="checkbox"
+ class="product-filter field-filter"
+ id="{{value}}"
+ data-filter-name="{{ item_field.fieldname }}"
+ data-filter-value="{{ value }}"
+ >
+ <span class="label-area">{{ value }}</span>
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <i class="text-muted">{{ _('No values') }}</i>
+ {% endif %}
+ </div>
+{% endfor %}
+{%- endmacro -%}
+
+{%- macro attribute_filter_section(filters)-%}
+{% for attribute in filters %}
+ <div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ attribute.name}}</div>
+ {% if values | len > 20 %}
+ <!-- show inline filter if values more than 20 -->
+ <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
+ {% endif %}
+
+ {% if attribute.item_attribute_values %}
+ <div class="filter-options">
+ {% for attr_value in attribute.item_attribute_values %}
+ <div class="checkbox">
+ <label data-value="{{ value }}">
+ <input type="checkbox"
+ class="product-filter attribute-filter"
+ id="{{attr_value.name}}"
+ data-attribute-name="{{ attribute.name }}"
+ data-attribute-value="{{ attr_value.attribute_value }}"
+ {% if attr_value.checked %} checked {% endif %}>
+ <span class="label-area">{{ attr_value.attribute_value }}</span>
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <i class="text-muted">{{ _('No values') }}</i>
+ {% endif %}
+ </div>
+{% endfor %}
+{%- endmacro -%}
+
+{%- macro discount_range_filters(filters)-%}
+<div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ _("Discounts") }}</div>
+ <div class="filter-options">
+ {% for entry in filters %}
+ <div class="checkbox">
+ <label data-value="{{ entry[0] }}">
+ <input type="radio" class="product-filter discount-filter"
+ name="discount" id="{{ entry[0] }}"
+ data-filter-name="discount" data-filter-value="{{ entry[0] }}"
+ >
+ <span class="label-area" for="{{ entry[0] }}">
+ {{ entry[1] }}
+ </span>
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+</div>
+{%- endmacro -%}
diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py
index 0b5e924..cbb430f 100644
--- a/erpnext/utilities/product.py
+++ b/erpnext/utilities/product.py
@@ -15,7 +15,7 @@
warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field)
if not warehouse and template_item_code and template_item_code != item_code:
- warehouse = frappe.db.get_value("Website Item", {"item_code": template_item_code }, item_warehouse_field)
+ warehouse = frappe.db.get_value("Website Item", {"item_code": template_item_code}, item_warehouse_field)
if warehouse:
stock_qty = frappe.db.sql("""
@@ -97,6 +97,7 @@
mrp = price_obj.price_list_rate or 0
if pricing_rule.pricing_rule_for == "Discount Percentage":
+ price_obj.discount_percent = pricing_rule.discount_percentage
price_obj.formatted_discount_percent = str(flt(pricing_rule.discount_percentage, 0)) + "%"
price_obj.price_list_rate = flt(price_obj.price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0)))
@@ -139,6 +140,6 @@
if frappe.db.exists("Product Bundle", item_code):
items = frappe.get_doc("Product Bundle", item_code).get_all_children()
bundle_warehouse = frappe.db.get_value('Item', item_code, item_warehouse_field)
- return all([ get_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items ])
+ return all([get_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items])
else:
return 1
diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html
index d32ef62..1e9b482 100644
--- a/erpnext/www/all-products/index.html
+++ b/erpnext/www/all-products/index.html
@@ -1,4 +1,6 @@
+{% from "erpnext/templates/includes/macros.html" import attribute_filter_section, field_filter_section, discount_range_filters %}
{% extends "templates/web.html" %}
+
{% block title %}{{ _('Products') }}{% endblock %}
{% block header %}
<div class="mb-6">{{ _('Products') }}</div>
@@ -53,71 +55,19 @@
<div class="mb-4 filters-title" > {{ _('Filters') }} </div>
<a class="mb-4 clear-filters" href="/all-products">{{ _('Clear All') }}</a>
</div>
+ <!-- field filters -->
{% if field_filters %}
- {% for field_filter in field_filters %}
- {%- set item_field = field_filter[0] %}
- {%- set values = field_filter[1] %}
- <div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ item_field.label }}</div>
-
- {% if values | len > 20 %}
- <!-- show inline filter if values more than 20 -->
- <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
- {% endif %}
-
- {% if values %}
- <div class="filter-options">
- {% for value in values %}
- <div class="checkbox" data-value="{{ value }}">
- <label for="{{value}}">
- <input type="checkbox"
- class="product-filter field-filter"
- id="{{value}}"
- data-filter-name="{{ item_field.fieldname }}"
- data-filter-value="{{ value }}"
- >
- <span class="label-area">{{ value }}</span>
- </label>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <i class="text-muted">{{ _('No values') }}</i>
- {% endif %}
- </div>
- {% endfor %}
+ {{ field_filter_section(field_filters) }}
{% endif %}
+ <!-- attribute filters -->
{% if attribute_filters %}
- {% for attribute in attribute_filters %}
- <div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ attribute.name}}</div>
- {% if values | len > 20 %}
- <!-- show inline filter if values more than 20 -->
- <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
- {% endif %}
+ {{ attribute_filter_section(attribute_filters) }}
+ {% endif %}
- {% if attribute.item_attribute_values %}
- <div class="filter-options">
- {% for attr_value in attribute.item_attribute_values %}
- <div class="checkbox">
- <label data-value="{{ value }}">
- <input type="checkbox"
- class="product-filter attribute-filter"
- id="{{attr_value.name}}"
- data-attribute-name="{{ attribute.name }}"
- data-attribute-value="{{ attr_value.attribute_value }}"
- {% if attr_value.checked %} checked {% endif %}>
- <span class="label-area">{{ attr_value.attribute_value }}</span>
- </label>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <i class="text-muted">{{ _('No values') }}</i>
- {% endif %}
- </div>
- {% endfor %}
+ <!-- discount filters -->
+ {% if discount_filters %}
+ {{ discount_range_filters(discount_filters) }}
{% endif %}
</div>
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index 3421709..d2a3b19 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -32,12 +32,16 @@
if (this.attribute_filters[attribute_name].length === 0) {
delete this.attribute_filters[attribute_name];
}
- } else if ($checkbox.is('.field-filter')) {
+ } 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);
diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py
index 618ec76..a4662bb 100644
--- a/erpnext/www/all-products/index.py
+++ b/erpnext/www/all-products/index.py
@@ -17,7 +17,7 @@
start = 0
engine = ProductQuery()
- context.items = engine.query(attribute_filters, field_filters, search, start)
+ context.items, discounts = engine.query(attribute_filters, field_filters, search, start)
# Add homepage as parent
context.parents = [{"name": frappe._("Home"), "route":"/"}]
@@ -26,6 +26,8 @@
context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_filters()
+ if discounts:
+ context.discount_filters = filter_engine.get_discount_filters(discounts)
context.e_commerce_settings = engine.settings
context.page_length = engine.settings.products_per_page or 20
@@ -39,12 +41,13 @@
attribute_filters = frappe.parse_json(attribute_filters)
engine = ProductQuery()
- items = engine.query(attribute_filters, field_filters, search_term=None, start=0)
+ items, discounts = engine.query(attribute_filters, field_filters, search_term=None, start=0)
item_html = []
for item in items:
item_html.append(frappe.render_template('erpnext/www/all-products/item_row.html', {
- 'item': item
+ 'item': item,
+ 'e_commerce_settings': None
}))
html = ''.join(item_html)