feat: Wishlist Page
- Navbar icon with badge count for wishlist
- Wishlist page with cards
- Cards can be moved to cart or removed in a click
- Separated all wishlist related methods into wishlist.js
- Made a common js method(util) to add/remove wishlist items
- Bug fix: Make sure items are removed from session user's wishlist
diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py
index 7527c6f..83bdff6 100644
--- a/erpnext/e_commerce/doctype/wishlist/wishlist.py
+++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py
@@ -10,18 +10,22 @@
pass
@frappe.whitelist()
-def add_to_wishlist(item_code, price):
+def add_to_wishlist(item_code, price, formatted_price=None):
"""Insert Item into wishlist."""
web_item_data = frappe.db.get_value("Website Item", {"item_code": item_code},
- ["image", "website_warehouse", "name", "item_name"], as_dict=1)
+ ["image", "website_warehouse", "name", "item_name", "item_group", "route"]
+ , as_dict=1)
wished_item_dict = {
"item_code": item_code,
"item_name": web_item_data.get("item_name"),
+ "item_group": web_item_data.get("item_group"),
"website_item": web_item_data.get("name"),
"price": frappe.utils.flt(price),
+ "formatted_price": formatted_price,
"image": web_item_data.get("image"),
- "website_warehouse": web_item_data.get("website_warehouse")
+ "warehouse": web_item_data.get("website_warehouse"),
+ "route": web_item_data.get("route")
}
if not frappe.db.exists("Wishlist", frappe.session.user):
diff --git a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json
index 18065a8..0b13273 100644
--- a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json
+++ b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json
@@ -9,13 +9,16 @@
"website_item",
"column_break_3",
"item_name",
+ "item_group",
"item_details_section",
"description",
"column_break_7",
- "section_break_8",
- "price",
+ "route",
"image",
"image_view",
+ "section_break_8",
+ "price",
+ "formatted_price",
"warehouse_section",
"warehouse"
],
@@ -101,12 +104,28 @@
"fieldname": "price",
"fieldtype": "Float",
"label": "Price"
+ },
+ {
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "fieldname": "route",
+ "fieldtype": "Small Text",
+ "label": "Route"
+ },
+ {
+ "fieldname": "formatted_price",
+ "fieldtype": "Data",
+ "label": "Formatted Price"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-03-12 18:23:03.487891",
+ "modified": "2021-03-15 16:37:40.405333",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Wishlist Items",
diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py
index 28d33e6..c37f8fb 100644
--- a/erpnext/e_commerce/product_query.py
+++ b/erpnext/e_commerce/product_query.py
@@ -81,7 +81,7 @@
item.in_stock = "green" if stock_qty else "red"
item.wished = False
- if frappe.db.exists("Wishlist Items", {"item_code": item.item_code}):
+ if frappe.db.exists("Wishlist Items", {"item_code": item.item_code, "parent": frappe.session.user}):
item.wished = True
return result
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
index bcfa983..b57862b 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -186,5 +186,40 @@
$(".shopping-cart").toggleClass('hidden', r.message ? false : true);
}
});
+ },
+
+ animate_add_to_cart(button) {
+ // Create 'added to cart' animation
+ let btn_id = "#" + button[0].id;
+ this.toggle_button_class(button, 'not-added', 'added-to-cart');
+ $(btn_id).text('Added to Cart');
+
+ // undo
+ setTimeout(() => {
+ this.toggle_button_class(button, 'added-to-cart', 'not-added');
+ $(btn_id).text('Add to Cart');
+ }, 2000);
+ },
+
+ toggle_button_class(button, remove, add) {
+ button.removeClass(remove);
+ button.addClass(add);
+ },
+
+ bind_add_to_cart_action() {
+ $('.page_content').on('click', '.btn-add-to-cart-list', (e) => {
+ const $btn = $(e.currentTarget);
+ $btn.prop('disabled', true);
+
+ this.animate_add_to_cart($btn);
+
+ const item_code = $btn.data('item-code');
+ erpnext.shopping_cart.update_cart({
+ item_code,
+ qty: 1
+ });
+
+ });
}
+
});
diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js
index 328bdb9..6ab1906 100644
--- a/erpnext/public/js/wishlist.js
+++ b/erpnext/public/js/wishlist.js
@@ -1,13 +1,12 @@
-frappe.provide("erpnext.e_commerce");
-var wishlist = erpnext.e_commerce;
+frappe.provide("erpnext.wishlist");
+var wishlist = erpnext.wishlist;
-frappe.ready(function() {
- $(".wishlist").toggleClass('hidden', true);
- wishlist.set_wishlist_count();
-});
+frappe.provide("erpnext.shopping_cart");
+var shopping_cart = erpnext.shopping_cart;
$.extend(wishlist, {
set_wishlist_count: function() {
+ // set badge count for wishlist icon
var wish_count = frappe.get_cookie("wish_count");
if(frappe.session.user==="Guest") {
wish_count = 0;
@@ -35,5 +34,127 @@
} else {
$badge.remove();
}
+ },
+
+ bind_move_to_cart_action: function() {
+ // move item to cart from wishlist
+ $('.page_content').on("click", ".btn-add-to-cart", (e) => {
+ const $move_to_cart_btn = $(e.currentTarget);
+ let item_code = $move_to_cart_btn.data("item-code");
+
+ shopping_cart.shopping_cart_update({
+ item_code,
+ qty: 1,
+ cart_dropdown: true
+ });
+
+ let success_action = function() {
+ const $card_wrapper = $move_to_cart_btn.closest(".item-card");
+ $card_wrapper.addClass("wish-removed");
+ };
+ let args = { item_code: item_code };
+ this.add_remove_from_wishlist("remove", args, success_action, null, true);
+ });
+ },
+
+ bind_remove_action: function() {
+ // remove item from wishlist
+ $('.page_content').on("click", ".remove-wish", (e) => {
+ const $remove_wish_btn = $(e.currentTarget);
+ let item_code = $remove_wish_btn.data("item-code");
+
+ let success_action = function() {
+ const $card_wrapper = $remove_wish_btn.closest(".item-card");
+ $card_wrapper.addClass("wish-removed");
+ };
+ let args = { item_code: item_code };
+ this.add_remove_from_wishlist("remove", args, success_action);
+ });
+ },
+
+ bind_wishlist_action() {
+ // 'wish'('like') or 'unwish' item in product listing
+ $('.page_content').on('click', '.like-action', (e) => {
+ const $btn = $(e.currentTarget);
+ const $wish_icon = $btn.find('.wish-icon');
+ let me = this;
+
+ let success_action = function() {
+ erpnext.wishlist.set_wishlist_count();
+ };
+
+ if ($wish_icon.hasClass('wished')) {
+ // un-wish item
+ $btn.removeClass("like-animate");
+ this.toggle_button_class($wish_icon, 'wished', 'not-wished');
+
+ let args = { item_code: $btn.data('item-code') };
+ let failure_action = function() {
+ me.toggle_button_class($wish_icon, 'not-wished', 'wished');
+ };
+ this.add_remove_from_wishlist("remove", args, success_action, failure_action);
+ } else {
+ // wish item
+ $btn.addClass("like-animate");
+ this.toggle_button_class($wish_icon, 'not-wished', 'wished');
+
+ let args = {
+ item_code: $btn.data('item-code'),
+ price: $btn.data('price'),
+ formatted_price: $btn.data('formatted-price')
+ };
+ let failure_action = function() {
+ me.toggle_button_class($wish_icon, 'wished', 'not-wished');
+ };
+ this.add_remove_from_wishlist("add", args, success_action, failure_action);
+ }
+ });
+ },
+
+ toggle_button_class(button, remove, add) {
+ button.removeClass(remove);
+ button.addClass(add);
+ },
+
+ add_remove_from_wishlist(action, args, success_action, failure_action, async=false) {
+ /* AJAX call to add or remove Item from Wishlist
+ action: "add" or "remove"
+ args: args for method (item_code, price, formatted_price),
+ success_action: method to execute on successs,
+ failure_action: method to execute on failure,
+ async: make call asynchronously (true/false). */
+ let method = "erpnext.e_commerce.doctype.wishlist.wishlist.add_to_wishlist";
+ if (action === "remove") {
+ method = "erpnext.e_commerce.doctype.wishlist.wishlist.remove_from_wishlist";
+ }
+
+ frappe.call({
+ type: "POST",
+ method: method,
+ args: args,
+ callback: function (r) {
+ if (r.exc) {
+ if (failure_action && (typeof failure_action === 'function')) {failure_action();}
+ frappe.msgprint({
+ message: __("Sorry, something went wrong. Please refresh."),
+ indicator: "red", title: __("Note")
+ });
+ } else {
+ if (success_action && (typeof success_action === 'function')) {success_action();}
+ }
+ }
+ });
}
+
+});
+
+frappe.ready(function() {
+ if (window.location.pathname !== "/wishlist") {
+ $(".wishlist").toggleClass('hidden', true);
+ wishlist.set_wishlist_count();
+ } else {
+ wishlist.bind_move_to_cart_action();
+ wishlist.bind_remove_action();
+ }
+
});
\ No newline at end of file
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 3d66f14..6a96e41 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -648,3 +648,27 @@
color: white;
}
}
+
+.wishlist-cart-not-added {
+ color: var(--blue-500);
+ background-color: white;
+ border: 1px solid var(--blue-500);
+ --icon-stroke: var(--blue-500);
+
+ &:hover {
+ background-color: var(--blue-500);
+ color: white;
+ --icon-stroke: white;
+ }
+}
+
+.remove-wish {
+ &:hover {
+ background-color: var(--gray-100);
+ border: 1px solid var(--icon-stroke);
+ }
+}
+
+.wish-removed {
+ display: none;
+}
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index 743daaf..aec201e 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -118,7 +118,6 @@
'text-left': align == 'Left' or is_featured,
}) -%}
<div class="card-body {{ align_class }}" style="width:100%">
-
<div style="margin-top: 16px; display: flex;">
<a href="/{{ item.route or '#' }}">
<div class="product-title">{{ title or '' }}</div>
@@ -128,7 +127,9 @@
{% endif %}
{% if not item.has_variants %}
<div class="like-action"
- data-item-code="{{ item.item_code }}" data-price="{{ item.price }}">
+ data-item-code="{{ item.item_code }}"
+ data-price="{{ item.price }}"
+ data-formatted-price="{{ item.get('formatted_price') }}">
<svg class="icon sm">
{%- set icon_class = "wished" if item.wished else "not-wished"-%}
<use class="{{ icon_class }} wish-icon" href="#icon-heart"></use>
@@ -161,3 +162,63 @@
{% endif %}
</div>
{%- endmacro -%}
+
+
+{%- macro wishlist_card(item, settings) %}
+<div class="col-sm-3 item-card" style="min-width: 220px;">
+ <div class="card text-center">
+ {% if item.image %}
+ <div class="card-img-container">
+ <a href="/{{ item.route or '#' }}" style="text-decoration: none;">
+ <img class="card-img" src="{{ item.image }}" alt="{{ title }}">
+ </a>
+ <div class="remove-wish"
+ style="position:absolute; top:10px; right: 20px; border-radius: 50%; border: 1px solid var(--gray-100); width: 25px; height: 25px;"
+ data-item-code="{{ item.item_code }}">
+ <span style="padding-bottom: 2px;">
+ <svg class="icon sm remove-wish-icon" style="margin-bottom: 4px; margin-left: 0.5px;">
+ <use class="close" href="#icon-close"></use>
+ </svg>
+ </span>
+ </div>
+
+ </div>
+ {% else %}
+ <a href="/{{ item.route or '#' }}" style="text-decoration: none;">
+ <div class="card-img-top no-image">
+ {{ frappe.utils.get_abbr(title) }}
+ </div>
+ </a>
+ {% endif %}
+
+ {{ wishlist_card_body(item, settings) }}
+
+
+ </div>
+</div>
+{%- endmacro -%}
+
+{%- macro wishlist_card_body(item, settings) %}
+<div class="card-body text-center" style="width:100%">
+ <div style="margin-top: 16px;">
+ <div class="product-title">{{ item.item_name or item.item_code or ''}}</div>
+ </div>
+ <div class="product-price">{{ item.formatted_price or '' }}</div>
+
+ {% if (item.available and settings.show_stock_availability) or (not settings.show_stock_availability) %}
+ <!-- Show move to cart button if in stock or if showing stock availability is disabled -->
+ <button data-item-code="{{ item.item_code}}" class="btn btn-add-to-cart w-100 wishlist-cart-not-added">
+ <span class="mr-2">
+ <svg class="icon icon-md">
+ <use href="#icon-assets"></use>
+ </svg>
+ </span>
+ {{ _("Move to Cart") }}
+ </button>
+ {% else %}
+ <div style="color: #F47A7A; width: 100%;">
+ {{ _("Not in Stock") }}
+ </div>
+ {% endif %}
+</div>
+{%- endmacro -%}
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
index 54ed98a..793bacb 100644
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ b/erpnext/templates/includes/navbar/navbar_items.html
@@ -10,7 +10,7 @@
</a>
</li>
<li class="wishlist wishlist-icon hidden">
- <a class="nav-link" href="/cart">
+ <a class="nav-link" href="/wishlist">
<svg class="icon icon-lg">
<use href="#icon-heart"></use>
</svg>
diff --git a/erpnext/templates/pages/wishlist.html b/erpnext/templates/pages/wishlist.html
new file mode 100644
index 0000000..6e7a65b
--- /dev/null
+++ b/erpnext/templates/pages/wishlist.html
@@ -0,0 +1,24 @@
+{% extends "templates/web.html" %}
+
+{% block title %} {{ _("Wishlist") }} {% endblock %}
+
+{% block header %}<h3 class="shopping-cart-header mt-2 mb-6">{{ _("Wishlist") }}</h1>{% endblock %}
+
+{% block page_content %}
+{% if items %}
+ <div class="row">
+ <div class="col-12 col-md-11 item-card-group-section">
+ <div class="row products-list">
+ {% from "erpnext/templates/includes/macros.html" import wishlist_card %}
+ {% for item in items %}
+ {{ wishlist_card(item, settings) }}
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+{% else %}
+ <!-- TODO: Make empty state for wishlist -->
+ {% include "erpnext/www/all-products/not_found.html" %}
+{% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py
new file mode 100644
index 0000000..15aef87
--- /dev/null
+++ b/erpnext/templates/pages/wishlist.py
@@ -0,0 +1,39 @@
+# 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.e_commerce.shopping_cart.cart import get_cart_quotation
+
+def get_context(context):
+ settings = frappe.get_doc("E Commerce Settings")
+ items = get_wishlist_items()
+
+ if settings.show_stock_availability:
+ for item in items:
+ stock_qty = frappe.utils.flt(
+ frappe.db.get_value("Bin",
+ {
+ "item_code": item.item_code,
+ "warehouse": item.get("warehouse")
+ },
+ "actual_qty")
+ )
+ item.available = True if stock_qty else False
+
+ context.items = items
+ context.settings = settings
+
+def get_wishlist_items():
+ if frappe.db.exists("Wishlist", frappe.session.user):
+ return frappe.db.sql("""
+ Select
+ item_code, item_name, website_item, price,
+ warehouse, image, item_group, route, formatted_price
+ from
+ `tabWishlist Items`
+ where
+ parent=%(user)s"""%{"user": frappe.db.escape(frappe.session.user)}, as_dict=1)
+ return
\ No newline at end of file
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index fc1a3f4..3421709 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -73,98 +73,8 @@
}
bind_card_actions() {
- this.bind_add_to_cart_action();
- this.bind_wishlist_action();
- }
-
- bind_add_to_cart_action() {
- $('.page_content').on('click', '.btn-add-to-cart-list', (e) => {
- const $btn = $(e.currentTarget);
- $btn.prop('disabled', true);
-
- this.animate_add_to_cart($btn);
-
- const item_code = $btn.data('item-code');
- erpnext.shopping_cart.update_cart({
- item_code,
- qty: 1
- });
-
- });
- }
-
- animate_add_to_cart(button) {
- // Create 'added to cart' animation
- let btn_id = "#" + button[0].id;
- this.toggle_button_class(button, 'not-added', 'added-to-cart');
- $(btn_id).text('Added to Cart');
-
- // undo
- setTimeout(() => {
- this.toggle_button_class(button, 'added-to-cart', 'not-added');
- $(btn_id).text('Add to Cart');
- }, 2000);
- }
-
- bind_wishlist_action() {
- $('.page_content').on('click', '.like-action', (e) => {
- const $btn = $(e.currentTarget);
- const $wish_icon = $btn.find('.wish-icon');
- let me = this;
-
- if ($wish_icon.hasClass('wished')) {
- // un-wish item
- $btn.removeClass("like-animate");
- this.toggle_button_class($wish_icon, 'wished', 'not-wished');
- frappe.call({
- type: "POST",
- method: "erpnext.e_commerce.doctype.wishlist.wishlist.remove_from_wishlist",
- args: {
- item_code: $btn.data('item-code')
- },
- callback: function (r) {
- if (r.exc) {
- me.toggle_button_class($wish_icon, 'wished', 'not-wished');
- frappe.msgprint({
- message: __("Sorry, something went wrong. Please refresh."),
- indicator: "red",
- title: __("Note")}
- );
- } else {
- erpnext.e_commerce.set_wishlist_count();
- }
- }
- });
- } else {
- $btn.addClass("like-animate");
- this.toggle_button_class($wish_icon, 'not-wished', 'wished');
- frappe.call({
- type: "POST",
- method: "erpnext.e_commerce.doctype.wishlist.wishlist.add_to_wishlist",
- args: {
- item_code: $btn.data('item-code'),
- price: $btn.data('price')
- },
- callback: function (r) {
- if (r.exc) {
- me.toggle_button_class($wish_icon, 'wished', 'not-wished');
- frappe.msgprint({
- message: __("Sorry, something went wrong. Please refresh."),
- indicator: "red",
- title: __("Note")}
- );
- } else {
- erpnext.e_commerce.set_wishlist_count();
- }
- }
- });
- }
- });
- }
-
- toggle_button_class(button, remove, add) {
- button.removeClass(remove);
- button.addClass(add);
+ erpnext.shopping_cart.bind_add_to_cart_action();
+ erpnext.wishlist.bind_wishlist_action();
}
bind_search() {