From b15ff57a6639e20ce06cec07c4a36d70fb1a70e1 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 25 Mar 2021 11:52:50 +0530 Subject: [PATCH] feat: (wip) Ratings and Reviews - Added Ratings and Reviews section in Item full page view - Added provision to write a review with popup - Created Item Review Doctype to store User-Item unique reviews - Added privision to enable/disable wishlist and reviews in e commerce settings - Hide cart and wishlist actions everywhere (even navbar) depending on settings - Moved some more inline css to scss - Small logic fixes TODO: Reviews full page view with paging --- .../e_commerce_settings.json | 28 +++- .../doctype/item_review/__init__.py | 0 .../doctype/item_review/item_review.js | 8 ++ .../doctype/item_review/item_review.json | 112 +++++++++++++++ .../doctype/item_review/item_review.py | 47 +++++++ .../doctype/item_review/test_item_review.py | 10 ++ .../doctype/website_item/website_item.py | 25 +++- .../e_commerce/doctype/wishlist/wishlist.json | 4 +- erpnext/e_commerce/product_query.py | 23 ++-- .../item_card_group/item_card_group.html | 2 +- erpnext/public/scss/shopping_cart.scss | 70 +++++++++- .../setup/doctype/item_group/item_group.py | 1 + erpnext/templates/generators/item/item.html | 5 + .../generators/item/item_add_to_cart.html | 43 +++--- .../generators/item/item_reviews.html | 127 ++++++++++++++++++ .../generators/item/item_specifications.html | 9 +- erpnext/templates/includes/macros.html | 30 ++++- .../includes/navbar/navbar_items.html | 18 +-- erpnext/templates/pages/reviews.html | 0 erpnext/www/all-products/item_row.html | 2 +- 20 files changed, 508 insertions(+), 56 deletions(-) create mode 100644 erpnext/e_commerce/doctype/item_review/__init__.py create mode 100644 erpnext/e_commerce/doctype/item_review/item_review.js create mode 100644 erpnext/e_commerce/doctype/item_review/item_review.json create mode 100644 erpnext/e_commerce/doctype/item_review/item_review.py create mode 100644 erpnext/e_commerce/doctype/item_review/test_item_review.py create mode 100644 erpnext/templates/generators/item/item_reviews.html create mode 100644 erpnext/templates/pages/reviews.html 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 75f9c31ce7..805a530d8d 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 @@ -20,6 +20,10 @@ "show_quantity_in_website", "show_apply_coupon_code_in_website", "allow_items_not_in_stock", + "add_ons_section", + "enable_wishlist", + "column_break_18", + "enable_reviews", "section_break_18", "company", "price_list", @@ -274,12 +278,34 @@ "fieldtype": "Link", "label": "Slideshow", "options": "Website Slideshow" + }, + { + "collapsible": 1, + "fieldname": "add_ons_section", + "fieldtype": "Section Break", + "label": "Add-ons" + }, + { + "default": "0", + "fieldname": "enable_wishlist", + "fieldtype": "Check", + "label": "Enable Wishlist" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enable_reviews", + "fieldtype": "Check", + "label": "Enable Reviews and Ratings" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-03-01 20:24:56.548673", + "modified": "2021-03-23 17:15:01.956630", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/doctype/item_review/__init__.py b/erpnext/e_commerce/doctype/item_review/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/e_commerce/doctype/item_review/item_review.js b/erpnext/e_commerce/doctype/item_review/item_review.js new file mode 100644 index 0000000000..a57c370287 --- /dev/null +++ b/erpnext/e_commerce/doctype/item_review/item_review.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Item Review', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/e_commerce/doctype/item_review/item_review.json b/erpnext/e_commerce/doctype/item_review/item_review.json new file mode 100644 index 0000000000..918f433935 --- /dev/null +++ b/erpnext/e_commerce/doctype/item_review/item_review.json @@ -0,0 +1,112 @@ +{ + "actions": [], + "beta": 1, + "creation": "2021-03-23 16:47:26.542226", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "website_item", + "user", + "customer", + "column_break_3", + "item", + "published_on", + "reviews_section", + "review_title", + "rating", + "comment" + ], + "fields": [ + { + "fieldname": "website_item", + "fieldtype": "Link", + "label": "Website Item", + "options": "Website Item" + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "website_item.item_code", + "fieldname": "item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "reviews_section", + "fieldtype": "Section Break", + "label": "Reviews" + }, + { + "fieldname": "rating", + "fieldtype": "Rating", + "in_list_view": 1, + "label": "Rating" + }, + { + "fieldname": "comment", + "fieldtype": "Small Text", + "label": "Comment" + }, + { + "fieldname": "review_title", + "fieldtype": "Data", + "label": "Review Title" + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "fieldname": "published_on", + "fieldtype": "Data", + "label": "Published on" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-03-24 22:27:28.094535", + "modified_by": "Administrator", + "module": "E-commerce", + "name": "Item Review", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py new file mode 100644 index 0000000000..bbb85b3d5d --- /dev/null +++ b/erpnext/e_commerce/doctype/item_review/item_review.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from datetime import datetime +import frappe +from frappe.model.document import Document + +from frappe.contacts.doctype.contact.contact import get_contact_name + +class ItemReview(Document): + pass + +@frappe.whitelist() +def add_item_review(web_item, title, rating, comment=None): + """ Add an Item Review by a user if non-existent. """ + if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}): + doc = frappe.get_doc({ + "doctype": "Item Review", + "user": frappe.session.user, + "customer": get_customer(), + "website_item": web_item, + "item": frappe.db.get_value("Website Item", web_item, "item_code"), + "review_title": title, + "rating": rating, + "comment": comment + }) + doc.published_on = datetime.today().strftime("%d %B %Y") + doc.insert() + +def get_customer(): + user = frappe.session.user + contact_name = get_contact_name(user) + customer = None + + if contact_name: + contact = frappe.get_doc('Contact', contact_name) + for link in contact.links: + if link.link_doctype == "Customer": + customer = link.link_name + break + + 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 diff --git a/erpnext/e_commerce/doctype/item_review/test_item_review.py b/erpnext/e_commerce/doctype/item_review/test_item_review.py new file mode 100644 index 0000000000..5e6d24989e --- /dev/null +++ b/erpnext/e_commerce/doctype/item_review/test_item_review.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestItemReview(unittest.TestCase): + pass diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 7424bc9fe3..d8ebffcca2 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -176,6 +176,7 @@ class WebsiteItem(WebsiteGenerator): self.set_metatags(context) self.set_shopping_cart_data(context) self.get_product_details_section(context) + self.get_reviews(context) context.wished = False if frappe.db.exists("Wishlist Items", {"item_code": self.item_code, "parent": frappe.session.user}): @@ -340,7 +341,7 @@ class WebsiteItem(WebsiteGenerator): def get_product_details_section(self, context): """ Get section with tabs or website specifications. """ context.show_tabs = self.show_tabbed_section - if self.show_tabbed_section and self.tabs: + if self.show_tabbed_section and (self.tabs or self.website_specifications): context.tabs = self.get_tabs() else: context.website_specifications = self.website_specifications @@ -361,6 +362,28 @@ class WebsiteItem(WebsiteGenerator): return tab_values + def get_reviews(self, context): + if context.shopping_cart.cart_settings.enable_reviews: + context.reviews = frappe.db.get_all("Item Review", filters={"item": self.item_code}, + fields=["*"], limit=4) + + rating_data = frappe.db.get_all("Item Review", filters={"item": self.item_code}, + fields=["avg(rating) as average, count(*) as total"])[0] + context.average_rating = rating_data.average + context.average_whole_rating = flt(context.average_rating, 0) + + # get % of reviews per rating + reviews_per_rating = [] + for i in range(1,6): + count = frappe.db.get_all("Item Review", filters={"item": self.item_code, "rating": i}, + fields=["count(*) as count"])[0].count + + percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0 + reviews_per_rating.append(percent) + + context.reviews_per_rating = reviews_per_rating + context.total_reviews = rating_data.total + def invalidate_cache_for_web_item(doc): """Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager.""" from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.json b/erpnext/e_commerce/doctype/wishlist/wishlist.json index 653c656fcc..ae24207d5f 100644 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.json +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.json @@ -34,7 +34,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-03-18 16:10:29.534522", + "modified": "2021-03-24 20:42:58.402031", "modified_by": "Administrator", "module": "E-commerce", "name": "Wishlist", @@ -55,7 +55,7 @@ "print": 1, "read": 1, "report": 1, - "role": "Stock Manager", + "role": "Website Manager", "share": 1 } ], diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py index c37f8fb6b2..6ffab56229 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -69,16 +69,19 @@ class ProductQuery: item.formatted_price = (product_info.get('price') or {}).get('formatted_price') item.price = product_info['price'].get('price_list_rate') - if self.settings.show_stock_availability and 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" + 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 item.wished = False if frappe.db.exists("Wishlist Items", {"item_code": item.item_code, "parent": frappe.session.user}): diff --git a/erpnext/e_commerce/web_template/item_card_group/item_card_group.html b/erpnext/e_commerce/web_template/item_card_group/item_card_group.html index 889a228168..33d7bccc23 100644 --- a/erpnext/e_commerce/web_template/item_card_group/item_card_group.html +++ b/erpnext/e_commerce/web_template/item_card_group/item_card_group.html @@ -26,7 +26,7 @@ {%- set item = frappe.get_doc("Item", item) -%} {{ item_card( item, is_featured=values['card_' + index + '_featured'], - True, "Center" + is_full_width=True, align="Center" ) }} {%- endif -%} {%- endfor -%} diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 0279f2285c..8d7f59de27 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -135,7 +135,7 @@ body.product-page { .item-card { padding: var(--padding-sm); - min-width: 250px; + min-width: 260px; } } @@ -702,3 +702,71 @@ body.product-page { .item-website-specification { font-size: .875rem; } + +.ratings-reviews-section { + border-top: 1px solid #E2E6E9; + display: flex; +} + +.reviews-header { + font-size: 20px; + font-weight: 600; + color: var(--gray-800); +} + +.rating-summary-title { + margin-top: 0.15rem; + font-size: 18px; +} + +.user-review-title { + margin-top: 0.15rem; + font-size: 16px; + font-weight: 600; +} + +.rating { + --star-fill: var(--gray-300); + .star-hover { + --star-fill: var(--yellow-100); + } + .star-click { + --star-fill: var(--yellow-300); + } +} + +.review { + max-width: 80%; + line-height: 1.6; + padding-bottom: 0.5rem; + border-bottom: 1px solid #E2E6E9; +} + +.review-signature { + display: flex; + font-size: 14px; + color: var(--gray-500); + font-weight: 400; + + .reviewer { + padding-right: 8px; + margin-right: 8px; + border-right: 1px solid var(--gray-400); + } +} + +.rating-progress-bar-section { + padding-bottom: 2rem; + border-bottom: 1px solid #E2E6E9; + margin-right: -10px; + + .rating-bar-title { + margin-left: -15px; + } + + .rating-progress-bar { + margin-bottom: 4px; + height: 7px; + margin-top: 6px; + } +} diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index c9924e19e2..a9e36799ad 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -72,6 +72,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): def get_context(self, context): context.show_search=True context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page')) or 6 + context.e_commerce_settings = frappe.get_cached_doc('E Commerce Settings', 'E Commerce Settings') context.search_link = '/product_search' if frappe.form_dict: diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html index eebfb9247d..5f027f7363 100644 --- a/erpnext/templates/generators/item/item.html +++ b/erpnext/templates/generators/item/item.html @@ -37,6 +37,11 @@ {{ doc.website_content or '' }} + + + {% if shopping_cart.cart_settings.enable_reviews %} + {% include "templates/generators/item/item_reviews.html"%} + {% endif %} diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index c7713c1219..3af360f253 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -57,34 +57,37 @@ {% endif %} - + {% if cart_settings.enable_wishlist %} + - {% set price = product_info.get("price") or {} %} - + + + + + + {{ _("Add to Wishlist") }} + + {% endif %} + {% if cart_settings.show_contact_us_button %} {% include "templates/generators/item/item_inquiry.html" %} {% endif %} diff --git a/erpnext/templates/generators/item/item_reviews.html b/erpnext/templates/generators/item/item_reviews.html new file mode 100644 index 0000000000..c271fdb808 --- /dev/null +++ b/erpnext/templates/generators/item/item_reviews.html @@ -0,0 +1,127 @@ +{% from "erpnext/templates/includes/macros.html" import ratings_with_title %} + +
+ +
+

+ {{ _("Customer Ratings") }} +

+ + {% if reviews %} + {% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") %} + {{ ratings_with_title(average_whole_rating, rating_title, "lg", "rating-summary-title") }} + {% endif %} + + +
+ {% for percent in reviews_per_rating %} +
+ {{ loop.index }} star +
+
+
+
+
+
+
+
+
+ {{ percent }}% +
+
+ {% endfor %} +
+ + + {% if frappe.session.user != "Guest" %} + + {% endif %} + +
+ + +
+

+ {{ _("Reviews") }} +

+ {% if reviews %} + {% for review in reviews %} + +
+ {{ ratings_with_title(review.rating, _(review.review_title), "md", "user-review-title") }} + +
+ {{ _(review.customer) }} + {{ review.published_on }} +
+
+

+ {{ _(review.comment) }} +

+
+
+ {% endfor %} + + {% if total_reviews > 4 %} + + {% endif %} + {% else %} +
+ {{ _("No Reviews") }} +
+ {% endif %} +
+
+ + diff --git a/erpnext/templates/generators/item/item_specifications.html b/erpnext/templates/generators/item/item_specifications.html index 1dccff9f92..f3957610e9 100644 --- a/erpnext/templates/generators/item/item_specifications.html +++ b/erpnext/templates/generators/item/item_specifications.html @@ -1,8 +1,9 @@ -{% if website_specifications -%} -
+ +{% if website_specifications %} +
{% if not show_tabs %} -

Product Details

+

Product Details

{% endif %} {% for d in website_specifications -%} @@ -14,4 +15,4 @@
-{%- endif %} +{% endif %} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 2d771b4c0a..e05bc636fd 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -59,7 +59,7 @@ {% endmacro %} -{%- macro item_card(item, is_featured=False, is_full_width=False, align="Left") -%} +{%- macro item_card(item, settings=None, is_featured=False, is_full_width=False, align="Left") -%} {%- set align_items_class = resolve_class({ 'align-items-end': align == 'Right', 'align-items-center': align == 'Center', @@ -79,12 +79,12 @@ {{ title }}
- {{ item_card_body(title, description, item, is_featured, align) }} + {{ item_card_body(title, settings, description, item, is_featured, align) }}
{% else %}
- {{ item_card_body(title, description, item, is_featured, align) }} + {{ item_card_body(title, settings, description, item, is_featured, align) }}
{% endif %} @@ -105,13 +105,13 @@ {% endif %} - {{ item_card_body(title, description, item, is_featured, align) }} + {{ item_card_body(title, settings, description, item, is_featured, align) }} {% endif %} {%- endmacro -%} -{%- macro item_card_body(title, description, item, is_featured, align) -%} +{%- macro item_card_body(title, settings, description, item, is_featured, align) -%} {%- set align_class = resolve_class({ 'text-right': align == 'Right', 'text-center': align == 'Center' and not is_featured, @@ -125,7 +125,7 @@ {% if item.in_stock %} {% endif %} - {% if not item.has_variants %} + {% if not item.has_variants and settings.enable_wishlist %}
- {% else %} + {% elif settings.enabled %}
{{ _('Add to Cart') }} @@ -220,3 +220,19 @@ {% endif %}
{%- endmacro -%} + +{%- macro ratings_with_title(avg_rating, title, size, rating_header_class) -%} +
+
+ {% for i in range(1,6) %} + {% set fill_class = 'star-click' if i <= avg_rating else '' %} + + + + {% endfor %} +
+

+ {{ title }} +

+
+{%- endmacro -%} diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html index 793bacb665..327552117b 100644 --- a/erpnext/templates/includes/navbar/navbar_items.html +++ b/erpnext/templates/includes/navbar/navbar_items.html @@ -9,12 +9,14 @@ - + {% if frappe.db.get_single_value("E Commerce Settings", "enable_wishlist") %} + + {% endif %} {% endblock %} diff --git a/erpnext/templates/pages/reviews.html b/erpnext/templates/pages/reviews.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/www/all-products/item_row.html b/erpnext/www/all-products/item_row.html index 072e35946b..538ce3b82f 100644 --- a/erpnext/www/all-products/item_row.html +++ b/erpnext/www/all-products/item_row.html @@ -1,4 +1,4 @@ {% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %} -{{ item_card(item) }} +{{ item_card(item, e_commerce_settings) }}