From 939b0dd67d8619ca3dc4ae60f3f7a95565dd3f45 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 10 Feb 2021 19:44:10 +0530 Subject: [PATCH 001/225] feat: E-commerce Refactor - Created "Website Item" to handle website related information - Publishing Item new flow - Created "E Commerce Settings" - Removed Products Settings --- .../__init__.py | 0 erpnext/e_commerce/doctype/__init__.py | 0 .../doctype/e_commerce_settings/__init__.py | 0 .../e_commerce_settings.js} | 4 +- .../e_commerce_settings.json | 104 +++++ .../e_commerce_settings.py} | 18 +- .../test_e_commerce_settings.py | 10 + .../doctype/website_item/__init__.py | 0 .../website_item/templates/website_item.html | 7 + .../templates/website_item_row.html | 4 + .../doctype/website_item/test_website_item.py | 10 + .../doctype/website_item/website_item.js | 8 + .../doctype/website_item/website_item.json | 279 +++++++++++++ .../doctype/website_item/website_item.py | 155 +++++++ .../doctype/website_item/website_item_list.js | 20 + erpnext/hooks.py | 2 +- erpnext/modules.txt | 1 + .../products_settings/products_settings.json | 389 ------------------ .../test_products_settings.py | 10 - .../test_product_configurator.py | 10 +- erpnext/portal/product_configurator/utils.py | 20 +- .../setup/doctype/item_group/item_group.py | 4 +- erpnext/shopping_cart/filters.py | 4 +- erpnext/shopping_cart/product_query.py | 4 +- erpnext/stock/doctype/item/item.js | 25 +- erpnext/stock/doctype/item/item.json | 19 +- erpnext/www/all-products/index.py | 10 +- 27 files changed, 672 insertions(+), 445 deletions(-) rename erpnext/{portal/doctype/products_settings => e_commerce}/__init__.py (100%) create mode 100644 erpnext/e_commerce/doctype/__init__.py create mode 100644 erpnext/e_commerce/doctype/e_commerce_settings/__init__.py rename erpnext/{portal/doctype/products_settings/products_settings.js => e_commerce/doctype/e_commerce_settings/e_commerce_settings.js} (84%) create mode 100644 erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json rename erpnext/{portal/doctype/products_settings/products_settings.py => e_commerce/doctype/e_commerce_settings/e_commerce_settings.py} (66%) create mode 100644 erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py create mode 100644 erpnext/e_commerce/doctype/website_item/__init__.py create mode 100644 erpnext/e_commerce/doctype/website_item/templates/website_item.html create mode 100644 erpnext/e_commerce/doctype/website_item/templates/website_item_row.html create mode 100644 erpnext/e_commerce/doctype/website_item/test_website_item.py create mode 100644 erpnext/e_commerce/doctype/website_item/website_item.js create mode 100644 erpnext/e_commerce/doctype/website_item/website_item.json create mode 100644 erpnext/e_commerce/doctype/website_item/website_item.py create mode 100644 erpnext/e_commerce/doctype/website_item/website_item_list.js delete mode 100644 erpnext/portal/doctype/products_settings/products_settings.json delete mode 100644 erpnext/portal/doctype/products_settings/test_products_settings.py diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/__init__.py similarity index 100% rename from erpnext/portal/doctype/products_settings/__init__.py rename to erpnext/e_commerce/__init__.py diff --git a/erpnext/e_commerce/doctype/__init__.py b/erpnext/e_commerce/doctype/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/__init__.py b/erpnext/e_commerce/doctype/e_commerce_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/portal/doctype/products_settings/products_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js similarity index 84% rename from erpnext/portal/doctype/products_settings/products_settings.js rename to erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js index 2f8b037164..d970f041be 100644 --- a/erpnext/portal/doctype/products_settings/products_settings.js +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js @@ -1,7 +1,7 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Products Settings', { +frappe.ui.form.on('E Commerce Settings', { refresh: function(frm) { frappe.model.with_doctype('Item', () => { const item_meta = frappe.get_meta('Item'); 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 new file mode 100644 index 0000000000..1a45adf6cd --- /dev/null +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -0,0 +1,104 @@ +{ + "actions": [], + "creation": "2021-02-10 17:13:39.139103", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "home_page_is_products", + "show_availability_status", + "hide_variants", + "column_break_4", + "products_per_page", + "filter_categories_section", + "enable_field_filters", + "filter_fields", + "enable_attribute_filters", + "filter_attributes" + ], + "fields": [ + { + "default": "0", + "description": "If checked, the Home page will be the default Item Group for the website", + "fieldname": "home_page_is_products", + "fieldtype": "Check", + "label": "Home Page is Products" + }, + { + "default": "0", + "fieldname": "show_availability_status", + "fieldtype": "Check", + "label": "Show Availability Status" + }, + { + "default": "6", + "fieldname": "products_per_page", + "fieldtype": "Int", + "label": "Products per Page" + }, + { + "fieldname": "filter_categories_section", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "default": "0", + "fieldname": "hide_variants", + "fieldtype": "Check", + "label": "Hide Variants" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enable_field_filters", + "fieldtype": "Check", + "label": "Enable Field Filters" + }, + { + "default": "0", + "fieldname": "enable_attribute_filters", + "fieldtype": "Check", + "label": "Enable Attribute Filters" + }, + { + "depends_on": "enable_field_filters", + "fieldname": "filter_fields", + "fieldtype": "Table", + "label": "Item Fields", + "options": "Website Filter Field" + }, + { + "depends_on": "enable_attribute_filters", + "fieldname": "filter_attributes", + "fieldtype": "Table", + "label": "Attributes", + "options": "Website Attribute" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-02-10 19:22:47.154104", + "modified_by": "Administrator", + "module": "E-commerce", + "name": "E Commerce Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/portal/doctype/products_settings/products_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py similarity index 66% rename from erpnext/portal/doctype/products_settings/products_settings.py rename to erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index d4f09b9c8c..90596d6b6f 100644 --- a/erpnext/portal/doctype/products_settings/products_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals @@ -10,7 +10,7 @@ from frappe.model.document import Document from frappe.utils import cint -class ProductsSettings(Document): +class ECommerceSettings(Document): def validate(self): if self.home_page_is_products: frappe.db.set_value("Website Settings", None, "home_page", "products") @@ -19,17 +19,17 @@ class ProductsSettings(Document): self.validate_field_filters() self.validate_attribute_filters() - frappe.clear_document_cache("Product Settings", "Product Settings") + frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings") def validate_field_filters(self): if not (self.enable_field_filters and self.filter_fields): return - item_meta = frappe.get_meta('Item') - valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ['Link', 'Table MultiSelect']] + item_meta = frappe.get_meta("Item") + valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]] for f in self.filter_fields: if f.fieldname not in valid_fields: - frappe.throw(_('Filter Fields Row #{0}: Fieldname {1} must be of type "Link" or "Table MultiSelect"').format(f.idx, f.fieldname)) + frappe.throw(_("Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'").format(f.idx, f.fieldname)) def validate_attribute_filters(self): if not (self.enable_attribute_filters and self.filter_attributes): return @@ -39,7 +39,7 @@ class ProductsSettings(Document): def home_page_is_products(doc, method): - '''Called on saving Website Settings''' - home_page_is_products = cint(frappe.db.get_single_value('Products Settings', 'home_page_is_products')) + """Called on saving Website Settings.""" + home_page_is_products = cint(frappe.db.get_single_value("E Commerce Settings", "home_page_is_products")) if home_page_is_products: - doc.home_page = 'products' + doc.home_page = "products" \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py new file mode 100644 index 0000000000..cf23266a29 --- /dev/null +++ b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.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 TestECommerceSettings(unittest.TestCase): + pass diff --git a/erpnext/e_commerce/doctype/website_item/__init__.py b/erpnext/e_commerce/doctype/website_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/e_commerce/doctype/website_item/templates/website_item.html b/erpnext/e_commerce/doctype/website_item/templates/website_item.html new file mode 100644 index 0000000000..db123090aa --- /dev/null +++ b/erpnext/e_commerce/doctype/website_item/templates/website_item.html @@ -0,0 +1,7 @@ +{% extends "templates/web.html" %} + +{% block page_content %} +

{{ title }}

+{% endblock %} + + \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html b/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html new file mode 100644 index 0000000000..d7014b453a --- /dev/null +++ b/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html @@ -0,0 +1,4 @@ +
+ {{ doc.title or doc.name }} +
+ diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py new file mode 100644 index 0000000000..e4386a30db --- /dev/null +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.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 TestWebsiteItem(unittest.TestCase): + pass diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js new file mode 100644 index 0000000000..ecea74bc78 --- /dev/null +++ b/erpnext/e_commerce/doctype/website_item/website_item.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('Website Item', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json new file mode 100644 index 0000000000..85a83e6d6e --- /dev/null +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -0,0 +1,279 @@ +{ + "actions": [], + "allow_guest_to_view": 1, + "allow_import": 1, + "autoname": "field:item_code", + "creation": "2021-02-09 21:06:14.441698", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "web_item_name", + "route", + "has_variants", + "variant_of", + "published", + "column_break_3", + "item_code", + "item_name", + "item_group", + "stock_uom", + "brand", + "image", + "display_section", + "website_image", + "website_image_alt", + "column_break_13", + "slideshow", + "thumbnail", + "section_break_17", + "website_warehouse", + "website_specifications", + "copy_from_item_group", + "column_break_27", + "web_long_description", + "section_break_6", + "ranking", + "set_meta_tags", + "column_break_22", + "website_item_groups", + "advanced_display_section", + "website_content" + ], + "fields": [ + { + "description": "Website display name", + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "web_item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Website Item Name", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "label": "Item Code", + "options": "Item", + "reqd": 1, + "unique": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Search and SEO" + }, + { + "fieldname": "route", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Route", + "no_copy": 1 + }, + { + "description": "Items with higher ranking will be shown higher", + "fieldname": "ranking", + "fieldtype": "Int", + "label": "Ranking" + }, + { + "description": "Show a slideshow at the top of the page", + "fieldname": "slideshow", + "fieldtype": "Link", + "label": "Slideshow", + "options": "Website Slideshow" + }, + { + "description": "Item Image (if not slideshow)", + "fieldname": "website_image", + "fieldtype": "Attach", + "label": "Website Image" + }, + { + "description": "Image Alternative Text", + "fieldname": "website_image_alt", + "fieldtype": "Data", + "label": "Image Description" + }, + { + "fieldname": "thumbnail", + "fieldtype": "Data", + "label": "Thumbnail", + "read_only": 1 + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "description": "Show Stock availability based on this warehouse.", + "fieldname": "website_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Website Warehouse", + "options": "Warehouse" + }, + { + "description": "List this Item in multiple groups on the website.", + "fieldname": "website_item_groups", + "fieldtype": "Table", + "label": "Website Item Groups", + "options": "Website Item Group" + }, + { + "fieldname": "set_meta_tags", + "fieldtype": "Button", + "label": "Set Meta Tags" + }, + { + "fieldname": "section_break_17", + "fieldtype": "Section Break", + "label": "Display Information" + }, + { + "fieldname": "copy_from_item_group", + "fieldtype": "Button", + "label": "Copy From Item Group" + }, + { + "fieldname": "website_specifications", + "fieldtype": "Table", + "label": "Website Specifications", + "options": "Item Website Specification" + }, + { + "fieldname": "web_long_description", + "fieldtype": "Text Editor", + "label": "Website Description" + }, + { + "description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.", + "fieldname": "website_content", + "fieldtype": "HTML Editor", + "label": "Website Content" + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Group", + "options": "Item Group", + "read_only": 1 + }, + { + "fetch_from": "item_code.image", + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "in_preview": 1, + "label": "Image", + "print_hide": 1 + }, + { + "default": "1", + "fieldname": "published", + "fieldtype": "Check", + "label": "Published" + }, + { + "default": "0", + "depends_on": "has_variants", + "fetch_from": "item_code.has_variants", + "fieldname": "has_variants", + "fieldtype": "Check", + "in_standard_filter": 1, + "label": "Has Variants", + "no_copy": 1, + "read_only": 1 + }, + { + "depends_on": "variant_of", + "fetch_from": "item_code.variant_of", + "fieldname": "variant_of", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_standard_filter": 1, + "label": "Variant Of", + "options": "Item", + "read_only": 1, + "search_index": 1, + "set_only_once": 1 + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "depends_on": "brand", + "fetch_from": "item_code.brand", + "fieldname": "brand", + "fieldtype": "Link", + "label": "Brand", + "options": "Brand" + }, + { + "collapsible": 1, + "fieldname": "advanced_display_section", + "fieldtype": "Section Break", + "label": "Advanced Display" + }, + { + "fieldname": "display_section", + "fieldtype": "Section Break", + "label": "Display Images" + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + } + ], + "has_web_view": 1, + "image_field": "image", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-02-10 14:22:41.628232", + "modified_by": "Administrator", + "module": "E-commerce", + "name": "Website Item", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "web_item_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py new file mode 100644 index 0000000000..1e0b12b4d6 --- /dev/null +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe.website.doctype.website_slideshow.website_slideshow import \ + get_slideshow + +from frappe.website.render import clear_cache +from frappe.website.website_generator import WebsiteGenerator + +from frappe.utils import cstr, random_string + +class WebsiteItem(WebsiteGenerator): + website = frappe._dict( + page_title_field="web_item_name", + condition_field="published", + template="templates/generators/item/item.html", + no_cache=1 + ) + + def onload(self): + super(WebsiteItem, self).onload() + + def validate(self): + super(WebsiteItem, self).validate() + + if not self.item_code: + frappe.throw(_("Item Code is required"), title=_("Mandatory")) + + self.validate_website_image() + self.make_thumbnail() + self.publish_unpublish_desk_item(publish=True) + + def on_trash(self): + self.publish_unpublish_desk_item(publish=False) + + def publish_unpublish_desk_item(self, publish=True): + if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish: + return # if already published don't publish again + frappe.db.set_value("Item", self.item_code, "published_in_website", publish) + + def make_route(self): + """Called from set_route in WebsiteGenerator.""" + if not self.route: + return cstr(frappe.db.get_value('Item Group', self.item_group, + 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) + + def validate_website_image(self): + if frappe.flags.in_import: + return + + """Validate if the website image is a public file""" + auto_set_website_image = False + if not self.website_image and self.image: + auto_set_website_image = True + self.website_image = self.image + + if not self.website_image: + return + + # find if website image url exists as public + file_doc = frappe.get_all("File", filters={ + "file_url": self.website_image + }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1) + + if file_doc: + file_doc = file_doc[0] + + if not file_doc: + if not auto_set_website_image: + frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name)) + + self.website_image = None + + elif file_doc.is_private: + if not auto_set_website_image: + frappe.msgprint(_("Website Image should be a public file or website URL")) + + self.website_image = None + + def make_thumbnail(self): + if frappe.flags.in_import: + return + + """Make a thumbnail of `website_image`""" + import requests.exceptions + + if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): + self.thumbnail = None + + if self.website_image and not self.thumbnail: + file_doc = None + + try: + file_doc = frappe.get_doc("File", { + "file_url": self.website_image, + "attached_to_doctype": "Item", + "attached_to_name": self.name + }) + except frappe.DoesNotExistError: + pass + # cleanup + frappe.local.message_log.pop() + + except requests.exceptions.HTTPError: + frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image)) + self.website_image = None + + except requests.exceptions.SSLError: + frappe.msgprint( + _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)) + self.website_image = None + + # for CSV import + if self.website_image and not file_doc: + try: + file_doc = frappe.get_doc({ + "doctype": "File", + "file_url": self.website_image, + "attached_to_doctype": "Item", + "attached_to_name": self.name + }).save() + + except IOError: + self.website_image = None + + if file_doc: + if not file_doc.thumbnail_url: + file_doc.make_thumbnail() + + self.thumbnail = file_doc.thumbnail_url + +@frappe.whitelist() +def make_website_item(doc): + if not doc: + return + doc = json.loads(doc) + + if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}): + message = _("Website Item already exists against {0}").format(frappe.bold(doc.get("item_code"))) + frappe.throw(message, title=_("Already Published"), indicator="blue") + + website_item = frappe.new_doc("Website Item") + website_item.web_item_name = doc.get("item_name") + + fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image", + "has_variants", "variant_of"] + for field in fields_to_map: + website_item.update({field: doc.get(field)}) + + website_item.save() + return [website_item.name, website_item.web_item_name] diff --git a/erpnext/e_commerce/doctype/website_item/website_item_list.js b/erpnext/e_commerce/doctype/website_item/website_item_list.js new file mode 100644 index 0000000000..21be9428eb --- /dev/null +++ b/erpnext/e_commerce/doctype/website_item/website_item_list.js @@ -0,0 +1,20 @@ +frappe.listview_settings['Website Item'] = { + add_fields: ["item_name", "web_item_name", "published", "image", "has_variants", "variant_of"], + filters: [["published", "=", "1"]], + + get_indicator: function(doc) { + if (doc.has_variants && doc.published) { + return [__("Template"), "orange", "has_variants,=,Yes|published,=,1"]; + } else if (doc.has_variants && !doc.published) { + return [__("Template"), "grey", "has_variants,=,Yes|published,=,0"]; + } else if (doc.variant_of && doc.published) { + return [__("Variant"), "blue", "published,=,1|variant_of,=," + doc.variant_of]; + } else if (doc.variant_of && !doc.published) { + return [__("Variant"), "grey", "published,=,0|variant_of,=," + doc.variant_of]; + } else if (doc.published) { + return [__("Published"), "green", "published,=,1"]; + } else { + return [__("Not Published"), "grey", "published,=,0"]; + } + } +}; \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 05f07f515c..684b13ab65 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -244,7 +244,7 @@ doc_events = { "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" }, "Website Settings": { - "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products" + "validate": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.home_page_is_products" }, "Tax Category": { "validate": "erpnext.regional.india.utils.validate_tax_category" diff --git a/erpnext/modules.txt b/erpnext/modules.txt index a9f94ce133..1c02db27ba 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -26,3 +26,4 @@ Communication Loan Management Payroll Telephony +E-commerce diff --git a/erpnext/portal/doctype/products_settings/products_settings.json b/erpnext/portal/doctype/products_settings/products_settings.json deleted file mode 100644 index 2cf8431497..0000000000 --- a/erpnext/portal/doctype/products_settings/products_settings.json +++ /dev/null @@ -1,389 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-04-22 09:11:55.272398", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "If checked, the Home page will be the default Item Group for the website", - "fieldname": "home_page_is_products", - "fieldtype": "Check", - "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": "Home Page is Products", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column 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, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "show_availability_status", - "fieldtype": "Check", - "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": "Show Availability Status", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "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": "Product Page", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "6", - "fieldname": "products_per_page", - "fieldtype": "Int", - "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": "Products per Page", - "length": 0, - "no_copy": 0, - "options": "", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enable_field_filters", - "fieldtype": "Check", - "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": "Enable Field Filters", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "enable_field_filters", - "fieldname": "filter_fields", - "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": "Item Fields", - "length": 0, - "no_copy": 0, - "options": "Website Filter Field", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enable_attribute_filters", - "fieldtype": "Check", - "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": "Enable Attribute Filters", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "enable_attribute_filters", - "fieldname": "filter_attributes", - "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": "Attributes", - "length": 0, - "no_copy": 0, - "options": "Website Attribute", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hide_variants", - "fieldtype": "Check", - "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": "Hide Variants", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-07 19:18:31.822309", - "modified_by": "Administrator", - "module": "Portal", - "name": "Products Settings", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Website Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/portal/doctype/products_settings/test_products_settings.py b/erpnext/portal/doctype/products_settings/test_products_settings.py deleted file mode 100644 index 5495cc9d96..0000000000 --- a/erpnext/portal/doctype/products_settings/test_products_settings.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -import unittest - - -class TestProductsSettings(unittest.TestCase): - pass diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index 5db74f2c40..27adcef91b 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -72,11 +72,11 @@ class TestProductConfigurator(unittest.TestCase): template_items = frappe.get_all('Item', {'show_in_website': 1}) variant_items = frappe.get_all('Item', {'show_variant_in_website': 1}) - products_settings = frappe.get_doc('Products Settings') - products_settings.enable_field_filters = 1 - products_settings.append('filter_fields', {'fieldname': 'item_group'}) - products_settings.append('filter_fields', {'fieldname': 'stock_uom'}) - products_settings.save() + e_commerce_settings = frappe.get_doc('E Commerce Settings') + e_commerce_settings.enable_field_filters = 1 + e_commerce_settings.append('filter_fields', {'fieldname': 'item_group'}) + e_commerce_settings.append('filter_fields', {'fieldname': 'stock_uom'}) + e_commerce_settings.save() html = get_html_for_route('all-products') diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index cf623c8d42..b0c5c3754b 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -7,8 +7,8 @@ from erpnext.shopping_cart.product_info import get_product_info_for_website def get_field_filter_data(): - product_settings = get_product_settings() - filter_fields = [row.fieldname for row in product_settings.filter_fields] + e_commerce_settings = get_e_commerce_settings() + filter_fields = [row.fieldname for row in e_commerce_settings.filter_fields] meta = frappe.get_meta('Item') fields = [df for df in meta.fields if df.fieldname in filter_fields] @@ -34,8 +34,8 @@ def get_field_filter_data(): def get_attribute_filter_data(): - product_settings = get_product_settings() - attributes = [row.attribute for row in product_settings.filter_attributes] + e_commerce_settings = get_e_commerce_settings() + attributes = [row.attribute for row in e_commerce_settings.filter_attributes] attribute_docs = [ frappe.get_doc('Item Attribute', attribute) for attribute in attributes ] @@ -306,9 +306,9 @@ def get_items_by_fields(field_filters): def get_items(filters=None, search=None): - start = frappe.form_dict.get('start', 0) - products_settings = get_product_settings() - page_length = products_settings.products_per_page + start = frappe.form_dict.start or 0 + e_commerce_settings = get_e_commerce_settings() + page_length = e_commerce_settings.products_per_page filters = filters or [] # convert to list of filters @@ -318,7 +318,7 @@ def get_items(filters=None, search=None): enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and') show_in_website_condition = '' - if products_settings.hide_variants: + if e_commerce_settings.hide_variants: show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and') else: show_in_website_condition = get_conditions([ @@ -440,7 +440,7 @@ def get_html_for_items(items): })) return html -def get_product_settings(): - doc = frappe.get_cached_doc('Products Settings') +def get_e_commerce_settings(): + doc = frappe.get_cached_doc('E Commerce Settings') doc.products_per_page = doc.products_per_page or 20 return doc diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index ab50a58c4f..2f03ee7183 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -74,7 +74,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): def get_context(self, context): context.show_search=True - context.page_length = cint(frappe.db.get_single_value('Products Settings', 'products_per_page')) or 6 + context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page')) or 6 context.search_link = '/product_search' if frappe.form_dict: @@ -239,7 +239,7 @@ def get_item_for_list_in_html(context): if (context.get("website_image") or "").startswith("files/"): context["website_image"] = "/" + quote(context["website_image"]) - context["show_availability_status"] = cint(frappe.db.get_single_value('Products Settings', + context["show_availability_status"] = cint(frappe.db.get_single_value('E Commerce Settings', 'show_availability_status')) products_template = 'templates/includes/products_as_list.html' diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 4787ae534c..ed32db2105 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -8,8 +8,8 @@ import frappe class ProductFiltersBuilder: def __init__(self, item_group=None): - if not item_group or item_group == "Products Settings": - self.doc = frappe.get_doc("Products Settings") + if not item_group or item_group == "E Commerce Settings": + self.doc = frappe.get_doc("E Commerce Settings") else: self.doc = frappe.get_doc("Item Group", item_group) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 5cc0505aed..d105ab898b 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -15,13 +15,13 @@ class ProductQuery: filters (TYPE): Description or_filters (list): Description page_length (Int): Length of page for the query - settings (Document): Products Settings DocType + settings (Document): E Commerce Settings DocType filters (list) or_filters (list) """ def __init__(self): - self.settings = frappe.get_doc("Products Settings") + self.settings = frappe.get_doc("E Commerce Settings") self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.page_length = self.settings.products_per_page or 20 self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 752a1fe732..fb2ccef48f 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -91,6 +91,29 @@ frappe.ui.form.on("Item", { erpnext.toggle_naming_series(); } + if(!frm.doc.published_in_website) { + frm.add_custom_button(__("Publish in Website"), function() { + frappe.call({ + method: "erpnext.e_commerce.doctype.website_item.website_item.make_website_item", + args: {doc: frm.doc}, + freeze: true, + freeze_message: __("Publishing Item ..."), + callback: function(result) { + frappe.msgprint({ + message: __("Website Item {0} has been created.", + [repl('%(item)s', { + item_encoded: encodeURIComponent(result.message[0]), + item: result.message[1] + })] + ), + title: __("Published"), + indicator: "green" + }); + } + }); + }, __('Actions')); + } + erpnext.item.edit_prices_button(frm); erpnext.item.toggle_attributes(frm); @@ -393,7 +416,7 @@ $.extend(erpnext.item, { edit_prices_button: function(frm) { frm.add_custom_button(__("Add / Edit Prices"), function() { frappe.set_route("List", "Item Price", {"item_code": frm.doc.name}); - }, __("View")); + }, __("Actions")); }, weight_to_validate: function(frm){ diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index db5caf9164..39cc9c75e2 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -117,6 +117,7 @@ "default_item_manufacturer", "default_manufacturer_part_no", "website_section", + "published_in_website", "show_in_website", "show_variant_in_website", "route", @@ -124,7 +125,6 @@ "slideshow", "website_image", "website_image_alt", - "thumbnail", "cb72", "website_warehouse", "website_item_groups", @@ -922,12 +922,6 @@ "fieldtype": "Attach", "label": "Website Image" }, - { - "fieldname": "thumbnail", - "fieldtype": "Data", - "label": "Thumbnail", - "read_only": 1 - }, { "fieldname": "cb72", "fieldtype": "Column Break" @@ -1069,6 +1063,15 @@ "fieldname": "website_image_alt", "fieldtype": "Data", "label": "Image Description" + }, + { + "default": "0", + "fieldname": "published_in_website", + "fieldtype": "Check", + "label": "Published in Website", + "no_copy": 1, + "read_only": 1, + "search_index": 1 } ], "has_web_view": 1, @@ -1078,7 +1081,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2021-08-26 12:23:07.277077", + "modified": "2021-08-27 12:23:07.277077", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py index df5258b238..67d51ca161 100644 --- a/erpnext/www/all-products/index.py +++ b/erpnext/www/all-products/index.py @@ -1,6 +1,7 @@ import frappe -from erpnext.portal.product_configurator.utils import get_product_settings +from erpnext.portal.product_configurator.utils import (get_products_for_website, get_e_commerce_settings, + get_field_filter_data, get_attribute_filter_data) from erpnext.shopping_cart.filters import ProductFiltersBuilder from erpnext.shopping_cart.product_query import ProductQuery @@ -23,14 +24,15 @@ def get_context(context): # Add homepage as parent context.parents = [{"name": frappe._("Home"), "route":"/"}] - product_settings = get_product_settings() + e_commerce_settings = get_e_commerce_settings() filter_engine = ProductFiltersBuilder() context.field_filters = filter_engine.get_field_filters() context.attribute_filters = filter_engine.get_attribute_filters() - context.product_settings = product_settings + context.e_commerce_settings = e_commerce_settings context.body_class = "product-page" - context.page_length = product_settings.products_per_page or 20 + context.page_length = e_commerce_settings.products_per_page or 20 context.no_cache = 1 + print(context) From eef9cf152f1db8fcc8cfa23e8f51a7735c70637b Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 16 Feb 2021 18:45:36 +0530 Subject: [PATCH 002/225] chore: Drive E-commerce via Website Item - Removed Shopping Cart Settings - Portal fully driven via E Commerce Settings - All Item listing querying will happen via ProductQuery engine only - Product Listing via Website Items - removed redundant code - Moved all website logic from Item to Website Item --- .../payment_request/payment_request.py | 4 +- erpnext/accounts/doctype/tax_rule/tax_rule.py | 2 +- .../e_commerce_settings.js | 38 ++- .../e_commerce_settings.json | 184 ++++++++++- .../e_commerce_settings.py | 83 ++++- .../test_e_commerce_settings.py | 37 ++- .../doctype/website_item/website_item.json | 27 +- .../doctype/website_item/website_item.py | 190 ++++++++++- erpnext/hooks.py | 4 +- erpnext/portal/product_configurator/utils.py | 294 ------------------ erpnext/portal/utils.py | 4 +- erpnext/public/js/shopping_cart.js | 2 +- .../selling/doctype/quotation/quotation.py | 2 +- .../setup/doctype/item_group/item_group.py | 7 +- .../setup_wizard/operations/company_setup.py | 2 +- .../operations/install_fixtures.py | 2 +- erpnext/shopping_cart/cart.py | 20 +- .../shopping_cart_settings/__init__.py | 0 .../shopping_cart_settings.js | 38 --- .../shopping_cart_settings.json | 212 ------------- .../shopping_cart_settings.py | 85 ----- .../test_shopping_cart_settings.py | 56 ---- erpnext/shopping_cart/product_info.py | 9 +- erpnext/shopping_cart/product_query.py | 120 +++---- erpnext/shopping_cart/test_shopping_cart.py | 4 +- erpnext/shopping_cart/utils.py | 9 +- erpnext/stock/doctype/item/item.py | 281 +---------------- .../templates/generators/item/item_image.html | 2 +- .../templates/includes/cart/cart_address.html | 2 +- .../templates/includes/products_as_list.html | 2 +- erpnext/templates/pages/order.py | 7 +- erpnext/www/all-products/index.js | 2 +- erpnext/www/all-products/index.py | 35 ++- 33 files changed, 667 insertions(+), 1099 deletions(-) delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 2c967497d5..c5b8b548cc 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -293,7 +293,7 @@ class PaymentRequest(Document): if not status: return - shopping_cart_settings = frappe.get_doc("Shopping Cart Settings") + shopping_cart_settings = frappe.get_doc("E Commerce Settings") if status in ["Authorized", "Completed"]: redirect_to = None @@ -443,7 +443,7 @@ def get_gateway_details(args): return get_payment_gateway_account(args.get("payment_gateway_account")) if args.order_type == "Shopping Cart": - payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account + payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account return get_payment_gateway_account(payment_gateway_account) gateway_account = get_payment_gateway_account({"is_default": 1}) diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 150498d6ac..9a63dfedbe 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -102,7 +102,7 @@ class TaxRule(Document): def validate_use_for_shopping_cart(self): '''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one''' if (not self.use_for_shopping_cart - and cint(frappe.db.get_single_value('Shopping Cart Settings', 'enabled')) + and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled')) and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})): self.use_for_shopping_cart = 1 diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js index d970f041be..131a5e439d 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js @@ -1,13 +1,34 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('E Commerce Settings', { +frappe.ui.form.on("E Commerce Settings", { + onload: function(frm) { + if(frm.doc.__onload && frm.doc.__onload.quotation_series) { + frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series; + frm.refresh_field("quotation_series"); + } + + frm.set_query('payment_gateway_account', function() { + return { 'filters': { 'payment_channel': "Email" } }; + }); + }, refresh: function(frm) { - frappe.model.with_doctype('Item', () => { + if (frm.doc.enabled) { + frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html( + `
${__("Follow these steps to create a landing page for your store")}: + + docs/store-landing-page + +
` + ); + } + + frappe.model.with_doctype("Item", () => { const item_meta = frappe.get_meta('Item'); const valid_fields = item_meta.fields.filter( - df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden + df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden ).map(df => ({ label: df.label, value: df.fieldname })); frm.fields_dict.filter_fields.grid.update_docfield_property( @@ -17,5 +38,16 @@ frappe.ui.form.on('E Commerce Settings', { 'fieldname', 'options', valid_fields ); }); + }, + enabled: function(frm) { + if (frm.doc.enabled === 1) { + frm.set_value('enable_variants', 1); + } + else { + frm.set_value('company', ''); + frm.set_value('price_list', ''); + frm.set_value('default_customer_group', ''); + frm.set_value('quotation_series', ''); + } } }); 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 1a45adf6cd..b1b1cae770 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 @@ -10,6 +10,30 @@ "hide_variants", "column_break_4", "products_per_page", + "display_settings_section", + "show_attachments", + "show_price", + "show_stock_availability", + "enable_variants", + "column_break_13", + "show_contact_us_button", + "show_quantity_in_website", + "show_apply_coupon_code_in_website", + "allow_items_not_in_stock", + "section_break_18", + "company", + "price_list", + "enabled", + "store_page_docs", + "column_break_21", + "default_customer_group", + "quotation_series", + "checkout_settings_section", + "enable_checkout", + "save_quotations_as_draft", + "column_break_27", + "payment_gateway_account", + "payment_success_url", "filter_categories_section", "enable_field_filters", "filter_fields", @@ -37,6 +61,7 @@ "label": "Products per Page" }, { + "collapsible": 1, "fieldname": "filter_categories_section", "fieldtype": "Section Break", "label": "Filters" @@ -76,12 +101,169 @@ "fieldtype": "Table", "label": "Attributes", "options": "Website Attribute" + }, + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enable Shopping Cart" + }, + { + "depends_on": "doc.enabled", + "fieldname": "store_page_docs", + "fieldtype": "HTML" + }, + { + "fieldname": "display_settings_section", + "fieldtype": "Section Break", + "label": "Display Settings" + }, + { + "default": "0", + "fieldname": "show_attachments", + "fieldtype": "Check", + "label": "Show Public Attachments" + }, + { + "default": "0", + "fieldname": "show_price", + "fieldtype": "Check", + "label": "Show Price" + }, + { + "default": "0", + "fieldname": "show_stock_availability", + "fieldtype": "Check", + "label": "Show Stock Availability" + }, + { + "default": "0", + "fieldname": "enable_variants", + "fieldtype": "Check", + "label": "Enable Variants" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "show_contact_us_button", + "fieldtype": "Check", + "label": "Show Contact Us Button" + }, + { + "default": "0", + "depends_on": "show_stock_availability", + "fieldname": "show_quantity_in_website", + "fieldtype": "Check", + "label": "Show Stock Quantity" + }, + { + "default": "0", + "fieldname": "show_apply_coupon_code_in_website", + "fieldtype": "Check", + "label": "Show Apply Coupon Code" + }, + { + "default": "0", + "fieldname": "allow_items_not_in_stock", + "fieldtype": "Check", + "label": "Allow items not in stock to be added to cart" + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "label": "Shopping Cart" + }, + { + "depends_on": "enabled", + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "mandatory_depends_on": "eval: doc.enabled === 1", + "options": "Company", + "remember_last_selected_value": 1 + }, + { + "depends_on": "enabled", + "description": "Prices will not be shown if Price List is not set", + "fieldname": "price_list", + "fieldtype": "Link", + "label": "Price List", + "mandatory_depends_on": "eval: doc.enabled === 1", + "options": "Price List" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "depends_on": "enabled", + "fieldname": "default_customer_group", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Customer Group", + "mandatory_depends_on": "eval: doc.enabled === 1", + "options": "Customer Group" + }, + { + "depends_on": "enabled", + "fieldname": "quotation_series", + "fieldtype": "Select", + "label": "Quotation Series", + "mandatory_depends_on": "eval: doc.enabled === 1" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:doc.enable_checkout", + "depends_on": "enabled", + "fieldname": "checkout_settings_section", + "fieldtype": "Section Break", + "label": "Checkout Settings" + }, + { + "default": "0", + "fieldname": "enable_checkout", + "fieldtype": "Check", + "label": "Enable Checkout" + }, + { + "default": "Orders", + "depends_on": "enable_checkout", + "description": "After payment completion redirect user to selected page.", + "fieldname": "payment_success_url", + "fieldtype": "Select", + "label": "Payment Success Url", + "mandatory_depends_on": "enable_checkout", + "options": "\nOrders\nInvoices\nMy Account" + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval: doc.enable_checkout == 0", + "fieldname": "save_quotations_as_draft", + "fieldtype": "Check", + "label": "Save Quotations as Draft" + }, + { + "depends_on": "enable_checkout", + "fieldname": "payment_gateway_account", + "fieldtype": "Link", + "label": "Payment Gateway Account", + "mandatory_depends_on": "enable_checkout", + "options": "Payment Gateway Account" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-02-10 19:22:47.154104", + "modified": "2021-02-11 18:22:14.556880", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index 90596d6b6f..3fabc1a9b5 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -2,15 +2,18 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals - import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import cint +from frappe.utils import get_datetime, get_datetime_str, now_datetime +class ShoppingCartSetupError(frappe.ValidationError): pass class ECommerceSettings(Document): + def onload(self): + self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series") + def validate(self): if self.home_page_is_products: frappe.db.set_value("Website Settings", None, "home_page", "products") @@ -19,6 +22,9 @@ class ECommerceSettings(Document): self.validate_field_filters() self.validate_attribute_filters() + if self.enabled: + self.validate_exchange_rates_exist() + frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings") def validate_field_filters(self): @@ -37,6 +43,79 @@ class ECommerceSettings(Document): # if attribute filters are enabled, hide_variants should be disabled self.hide_variants = 0 + def validate_exchange_rates_exist(self): + """check if exchange rates exist for all Price List currencies (to company's currency)""" + company_currency = frappe.get_cached_value('Company', self.company, "default_currency") + if not company_currency: + msgprint(_("Please specify currency in Company") + ": " + self.company, + raise_exception=ShoppingCartSetupError) + + price_list_currency_map = frappe.db.get_values("Price List", + [self.price_list], "currency") + + price_list_currency_map = dict(price_list_currency_map) + + # check if all price lists have a currency + for price_list, currency in price_list_currency_map.items(): + if not currency: + frappe.throw(_("Currency is required for Price List {0}").format(price_list)) + + expected_to_exist = [currency + "-" + company_currency + for currency in price_list_currency_map.values() + if currency != company_currency] + + # manqala 20/09/2016: set up selection parameters for query from tabCurrency Exchange + from_currency = [currency for currency in price_list_currency_map.values() if currency != company_currency] + to_currency = company_currency + # manqala end + + if expected_to_exist: + # manqala 20/09/2016: modify query so that it uses date in the selection from Currency Exchange. + # exchange rates defined with date less than the date on which this document is being saved will be selected + exists = frappe.db.sql_list("""select CONCAT(from_currency,'-',to_currency) from `tabCurrency Exchange` + where from_currency in (%s) and to_currency = "%s" and date <= curdate()""" % (", ".join(["%s"]*len(from_currency)), to_currency), tuple(from_currency)) + # manqala end + + missing = list(set(expected_to_exist).difference(exists)) + + if missing: + msgprint(_("Missing Currency Exchange Rates for {0}").format(comma_and(missing)), + raise_exception=ShoppingCartSetupError) + + def validate_tax_rule(self): + if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"): + frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError) + + def get_tax_master(self, billing_territory): + tax_master = self.get_name_from_territory(billing_territory, "sales_taxes_and_charges_masters", + "sales_taxes_and_charges_master") + return tax_master and tax_master[0] or None + + def get_shipping_rules(self, shipping_territory): + return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule") + +def validate_cart_settings(doc, method): + frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate") + +def get_shopping_cart_settings(): + if not getattr(frappe.local, "shopping_cart_settings", None): + frappe.local.shopping_cart_settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings") + + return frappe.local.shopping_cart_settings + +@frappe.whitelist(allow_guest=True) +def is_cart_enabled(): + return get_shopping_cart_settings().enabled + +def show_quantity_in_website(): + return get_shopping_cart_settings().show_quantity_in_website + +def check_shopping_cart_enabled(): + if not get_shopping_cart_settings().enabled: + frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError) + +def show_attachments(): + return get_shopping_cart_settings().show_attachments def home_page_is_products(doc, method): """Called on saving Website Settings.""" diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py index cf23266a29..798529b222 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py @@ -2,9 +2,40 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - -# import frappe +import frappe import unittest +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ShoppingCartSetupError + class TestECommerceSettings(unittest.TestCase): - pass + def setUp(self): + frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """) + + def get_cart_settings(self): + return frappe.get_doc({"doctype": "E Commerce Settings", + "company": "_Test Company"}) + + def test_exchange_rate_exists(self): + frappe.db.sql("""delete from `tabCurrency Exchange`""") + + cart_settings = self.get_cart_settings() + cart_settings.price_list = "_Test Price List Rest of the World" + self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist) + + from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ + currency_exchange_records + frappe.get_doc(currency_exchange_records[0]).insert() + cart_settings.validate_exchange_rates_exist() + + def test_tax_rule_validation(self): + frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") + frappe.db.commit() + + cart_settings = self.get_cart_settings() + cart_settings.enabled = 1 + if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): + self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule) + + frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1") + +test_dependencies = ["Tax Rule"] diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index 85a83e6d6e..02717eae14 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -2,12 +2,13 @@ "actions": [], "allow_guest_to_view": 1, "allow_import": 1, - "autoname": "field:item_code", + "autoname": "naming_series", "creation": "2021-02-09 21:06:14.441698", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "naming_series", "web_item_name", "route", "has_variants", @@ -28,6 +29,7 @@ "thumbnail", "section_break_17", "website_warehouse", + "description", "website_specifications", "copy_from_item_group", "column_break_27", @@ -60,8 +62,8 @@ "fieldtype": "Link", "label": "Item Code", "options": "Item", - "reqd": 1, - "unique": 1 + "read_only_depends_on": "eval:!doc.__islocal", + "reqd": 1 }, { "fetch_from": "item_code.item_name", @@ -246,13 +248,30 @@ { "fieldname": "column_break_22", "fieldtype": "Column Break" + }, + { + "fetch_from": "item_code.description", + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Item Description", + "read_only": 1 + }, + { + "default": "WEB-ITM-.####", + "fieldname": "naming_series", + "fieldtype": "Select", + "hidden": 1, + "label": "Naming Series", + "no_copy": 1, + "options": "WEB-ITM-.####", + "print_hide": 1 } ], "has_web_view": 1, "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-10 14:22:41.628232", + "modified": "2021-02-12 16:49:42.275517", "modified_by": "Administrator", "module": "E-commerce", "name": "Website Item", diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 1e0b12b4d6..55436f257d 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -5,13 +5,17 @@ from __future__ import unicode_literals import frappe import json +import itertools +from frappe import _ +from six import iteritems from frappe.website.doctype.website_slideshow.website_slideshow import \ get_slideshow from frappe.website.render import clear_cache from frappe.website.website_generator import WebsiteGenerator +from frappe.utils import cstr, random_string, cint -from frappe.utils import cstr, random_string +from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups class WebsiteItem(WebsiteGenerator): website = frappe._dict( @@ -30,13 +34,23 @@ class WebsiteItem(WebsiteGenerator): if not self.item_code: frappe.throw(_("Item Code is required"), title=_("Mandatory")) + self.validate_duplicate_website_item() self.validate_website_image() self.make_thumbnail() self.publish_unpublish_desk_item(publish=True) + def on_update(self): + self.update_template_item() + def on_trash(self): self.publish_unpublish_desk_item(publish=False) + def validate_duplicate_website_item(self): + existing_web_item = frappe.db.exists("Website Item", {"item_code": self.item_code}) + if existing_web_item and existing_web_item != self.name: + message = _("Website Item already exists against Item {0}").format(frappe.bold(self.item_code)) + frappe.throw(message, title=_("Already Published")) + def publish_unpublish_desk_item(self, publish=True): if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish: return # if already published don't publish again @@ -48,6 +62,18 @@ class WebsiteItem(WebsiteGenerator): return cstr(frappe.db.get_value('Item Group', self.item_group, 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) + def update_template_item(self): + """Set Show in Website for Template Item if True for its Variant""" + if self.variant_of: + if self.published: + # show template + template_item = frappe.get_doc("Item", self.variant_of) + + if not template_item.published: + template_item.published = 1 + template_item.flags.ignore_permissions = True + template_item.save() + def validate_website_image(self): if frappe.flags.in_import: return @@ -133,6 +159,164 @@ class WebsiteItem(WebsiteGenerator): self.thumbnail = file_doc.thumbnail_url + def get_context(self, context): + print(context) + context.show_search = True + context.search_link = '/search' + + context.parents = get_parent_item_groups(self.item_group) + context.body_class = "product-page" + self.attributes = frappe.get_all("Item Variant Attribute", + fields=["attribute", "attribute_value"], + filters={"parent": self.item_code}) + self.set_variant_context(context) + self.set_attribute_context(context) + self.set_disabled_attributes(context) + self.set_metatags(context) + self.set_shopping_cart_data(context) + print("IN WEB ITEM") + + return context + + def set_variant_context(self, context): + if self.has_variants: + context.no_cache = True + + # load variants + # also used in set_attribute_context + context.variants = frappe.get_all("Item", + filters={"variant_of": self.name, "show_variant_in_website": 1}, + order_by="name asc") + + variant = frappe.form_dict.variant + if not variant and context.variants: + # the case when the item is opened for the first time from its list + variant = context.variants[0] + + if variant: + context.variant = frappe.get_doc("Item", variant) + + for fieldname in ("website_image", "website_image_alt", "web_long_description", "description", + "website_specifications"): + if context.variant.get(fieldname): + value = context.variant.get(fieldname) + if isinstance(value, list): + value = [d.as_dict() for d in value] + + context[fieldname] = value + + def set_attribute_context(self, context): + if self.has_variants: + attribute_values_available = {} + context.attribute_values = {} + context.selected_attributes = {} + + # load attributes + for v in context.variants: + v.attributes = frappe.get_all("Item Variant Attribute", + fields=["attribute", "attribute_value"], + filters={"parent": v.name}) + # make a map for easier access in templates + v.attribute_map = frappe._dict({}) + for attr in v.attributes: + v.attribute_map[attr.attribute] = attr.attribute_value + + for attr in v.attributes: + values = attribute_values_available.setdefault(attr.attribute, []) + if attr.attribute_value not in values: + values.append(attr.attribute_value) + + if v.name == context.variant.name: + context.selected_attributes[attr.attribute] = attr.attribute_value + + # filter attributes, order based on attribute table + for attr in self.attributes: + values = context.attribute_values.setdefault(attr.attribute, []) + + if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): + for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): + values.append(val) + + else: + # get list of values defined (for sequence) + for attr_value in frappe.db.get_all("Item Attribute Value", + fields=["attribute_value"], + filters={"parent": attr.attribute}, order_by="idx asc"): + + if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): + values.append(attr_value.attribute_value) + + context.variant_info = json.dumps(context.variants) + + def set_disabled_attributes(self, context): + """Disable selection options of attribute combinations that do not result in a variant""" + + if not self.attributes or not self.has_variants: + return + + context.disabled_attributes = {} + attributes = [attr.attribute for attr in self.attributes] + + def find_variant(combination): + for variant in context.variants: + if len(variant.attributes) < len(attributes): + continue + + if "combination" not in variant: + ref_combination = [] + + for attr in variant.attributes: + idx = attributes.index(attr.attribute) + ref_combination.insert(idx, attr.attribute_value) + + variant["combination"] = ref_combination + + if not (set(combination) - set(variant["combination"])): + # check if the combination is a subset of a variant combination + # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5] + return True + + for i, attr in enumerate(self.attributes): + if i == 0: + continue + + combination_source = [] + + # loop through previous attributes + for prev_attr in self.attributes[:i]: + combination_source.append([context.selected_attributes.get(prev_attr.attribute)]) + + combination_source.append(context.attribute_values[attr.attribute]) + + for combination in itertools.product(*combination_source): + if not find_variant(combination): + context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1]) + + def set_metatags(self, context): + context.metatags = frappe._dict({}) + + safe_description = frappe.utils.to_markdown(self.description) + + context.metatags.url = frappe.utils.get_url() + '/' + context.route + + if context.website_image: + if context.website_image.startswith('http'): + url = context.website_image + else: + url = frappe.utils.get_url() + context.website_image + context.metatags.image = url + + context.metatags.description = safe_description[:300] + + context.metatags.title = self.item_name or self.item_code + + context.metatags['og:type'] = 'product' + context.metatags['og:site_name'] = 'ERPNext' + + def set_shopping_cart_data(self, context): + from erpnext.shopping_cart.product_info import get_product_info_for_website + context.shopping_cart = get_product_info_for_website(self.item_code, skip_quotation_creation=True) + @frappe.whitelist() def make_website_item(doc): if not doc: @@ -141,13 +325,13 @@ def make_website_item(doc): if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}): message = _("Website Item already exists against {0}").format(frappe.bold(doc.get("item_code"))) - frappe.throw(message, title=_("Already Published"), indicator="blue") + frappe.throw(message, title=_("Already Published")) website_item = frappe.new_doc("Website Item") website_item.web_item_name = doc.get("item_name") fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image", - "has_variants", "variant_of"] + "has_variants", "variant_of", "description"] for field in fields_to_map: website_item.update({field: doc.get(field)}) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 684b13ab65..d0142d3d0d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -240,8 +240,8 @@ doc_events = { "erpnext.support.doctype.issue.issue.set_first_response_time" ] }, - "Sales Taxes and Charges Template": { - "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" + ("Sales Taxes and Charges Template", "Price List"): { + "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings" }, "Website Settings": { "validate": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.home_page_is_products" diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index b0c5c3754b..0de97294f3 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -5,166 +5,6 @@ from erpnext.portal.product_configurator.item_variants_cache import ItemVariants from erpnext.setup.doctype.item_group.item_group import get_child_groups from erpnext.shopping_cart.product_info import get_product_info_for_website - -def get_field_filter_data(): - e_commerce_settings = get_e_commerce_settings() - filter_fields = [row.fieldname for row in e_commerce_settings.filter_fields] - - meta = frappe.get_meta('Item') - fields = [df for df in meta.fields if df.fieldname in filter_fields] - - filter_data = [] - for f in fields: - doctype = f.get_link_doctype() - - # apply enable/disable/show_in_website filter - meta = frappe.get_meta(doctype) - filters = {} - if meta.has_field('enabled'): - filters['enabled'] = 1 - if meta.has_field('disabled'): - filters['disabled'] = 0 - if meta.has_field('show_in_website'): - filters['show_in_website'] = 1 - - values = [d.name for d in frappe.get_all(doctype, filters)] - filter_data.append([f, values]) - - return filter_data - - -def get_attribute_filter_data(): - e_commerce_settings = get_e_commerce_settings() - attributes = [row.attribute for row in e_commerce_settings.filter_attributes] - attribute_docs = [ - frappe.get_doc('Item Attribute', attribute) for attribute in attributes - ] - - # mark attribute values as checked if they are present in the request url - if frappe.form_dict: - for attr in attribute_docs: - if attr.name in frappe.form_dict: - value = frappe.form_dict[attr.name] - if value: - enabled_values = value.split(',') - else: - enabled_values = [] - - for v in enabled_values: - for item_attribute_row in attr.item_attribute_values: - if v == item_attribute_row.attribute_value: - item_attribute_row.checked = True - - return attribute_docs - - -def get_products_for_website(field_filters=None, attribute_filters=None, search=None): - if attribute_filters: - item_codes = get_item_codes_by_attributes(attribute_filters) - items_by_attributes = get_items([['name', 'in', item_codes]]) - - if field_filters: - items_by_fields = get_items_by_fields(field_filters) - - if attribute_filters and not field_filters: - return items_by_attributes - - if field_filters and not attribute_filters: - return items_by_fields - - if field_filters and attribute_filters: - items_intersection = [] - item_codes_in_attribute = [item.name for item in items_by_attributes] - - for item in items_by_fields: - if item.name in item_codes_in_attribute: - items_intersection.append(item) - - return items_intersection - - if search: - return get_items(search=search) - - return get_items() - - -@frappe.whitelist(allow_guest=True) -def get_products_html_for_website(field_filters=None, attribute_filters=None): - field_filters = frappe.parse_json(field_filters) - attribute_filters = frappe.parse_json(attribute_filters) - set_item_group_filters(field_filters) - - items = get_products_for_website(field_filters, attribute_filters) - html = ''.join(get_html_for_items(items)) - - if not items: - html = frappe.render_template('erpnext/www/all-products/not_found.html', {}) - - return html - -def set_item_group_filters(field_filters): - if field_filters is not None and 'item_group' in field_filters: - field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])] - - -def get_item_codes_by_attributes(attribute_filters, template_item_code=None): - items = [] - - for attribute, values in attribute_filters.items(): - attribute_values = values - - if not isinstance(attribute_values, list): - attribute_values = [attribute_values] - - if not attribute_values: continue - - wheres = [] - query_values = [] - for attribute_value in attribute_values: - wheres.append('( attribute = %s and attribute_value = %s )') - query_values += [attribute, attribute_value] - - attribute_query = ' or '.join(wheres) - - if template_item_code: - variant_of_query = 'AND t2.variant_of = %s' - query_values.append(template_item_code) - else: - variant_of_query = '' - - query = ''' - SELECT - t1.parent - FROM - `tabItem Variant Attribute` t1 - WHERE - 1 = 1 - AND ( - {attribute_query} - ) - AND EXISTS ( - SELECT - 1 - FROM - `tabItem` t2 - WHERE - t2.name = t1.parent - {variant_of_query} - ) - GROUP BY - t1.parent - ORDER BY - NULL - '''.format(attribute_query=attribute_query, variant_of_query=variant_of_query) - - item_codes = set([r[0] for r in frappe.db.sql(query, query_values)]) - items.append(item_codes) - - res = list(set.intersection(*items)) - - return res - - @frappe.whitelist(allow_guest=True) def get_attributes_and_values(item_code): '''Build a list of attributes and their possible values. @@ -278,140 +118,6 @@ def get_items_with_selected_attributes(item_code, selected_attributes): return set.intersection(*items) - -def get_items_by_fields(field_filters): - meta = frappe.get_meta('Item') - filters = [] - for fieldname, values in field_filters.items(): - if not values: continue - - _doctype = 'Item' - _fieldname = fieldname - - df = meta.get_field(fieldname) - if df.fieldtype == 'Table MultiSelect': - child_doctype = df.options - child_meta = frappe.get_meta(child_doctype) - fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) - if fields: - _doctype = child_doctype - _fieldname = fields[0].fieldname - - if len(values) == 1: - filters.append([_doctype, _fieldname, '=', values[0]]) - else: - filters.append([_doctype, _fieldname, 'in', values]) - - return get_items(filters) - - -def get_items(filters=None, search=None): - start = frappe.form_dict.start or 0 - e_commerce_settings = get_e_commerce_settings() - page_length = e_commerce_settings.products_per_page - - filters = filters or [] - # convert to list of filters - if isinstance(filters, dict): - filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()] - - enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and') - - show_in_website_condition = '' - if e_commerce_settings.hide_variants: - show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and') - else: - show_in_website_condition = get_conditions([ - ['show_in_website', '=', 1], - ['show_variant_in_website', '=', 1] - ], 'or') - - search_condition = '' - if search: - # Default fields to search from - default_fields = {'name', 'item_name', 'description', 'item_group'} - - # Get meta search fields - meta = frappe.get_meta("Item") - meta_fields = set(meta.get_search_fields()) - - # 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 - - # Build or filters for query - search = '%{}%'.format(search) - or_filters = [[field, 'like', search] for field in search_fields] - - search_condition = get_conditions(or_filters, 'or') - - filter_condition = get_conditions(filters, 'and') - - where_conditions = ' and '.join( - [condition for condition in [enabled_items_filter, show_in_website_condition, \ - search_condition, filter_condition] if condition] - ) - - left_joins = [] - for f in filters: - if len(f) == 4 and f[0] != 'Item': - left_joins.append(f[0]) - - left_join = ' '.join(['LEFT JOIN `tab{0}` on (`tab{0}`.parent = `tabItem`.name)'.format(l) for l in left_joins]) - - results = frappe.db.sql(''' - SELECT - `tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`, - `tabItem`.`website_image`, `tabItem`.`image`, - `tabItem`.`web_long_description`, `tabItem`.`description`, - `tabItem`.`route`, `tabItem`.`item_group` - FROM - `tabItem` - {left_join} - WHERE - {where_conditions} - GROUP BY - `tabItem`.`name` - ORDER BY - `tabItem`.`weightage` DESC - LIMIT - {page_length} - OFFSET - {start} - '''.format( - where_conditions=where_conditions, - start=start, - page_length=page_length, - left_join=left_join - ) - , as_dict=1) - - for r in results: - r.description = r.web_long_description or r.description - r.image = r.website_image or r.image - product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info') - if product_info: - r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None - - return results - - -def get_conditions(filter_list, and_or='and'): - from frappe.model.db_query import DatabaseQuery - - if not filter_list: - return '' - - conditions = [] - DatabaseQuery('Item').build_filter_conditions(filter_list, conditions, ignore_permissions=True) - join_by = ' {0} '.format(and_or) - - return '(' + join_by.join(conditions) + ')' - # utilities def get_item_attributes(item_code): diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py index bae8f353b3..3ee2c88939 100644 --- a/erpnext/portal/utils.py +++ b/erpnext/portal/utils.py @@ -1,10 +1,8 @@ -from __future__ import unicode_literals - import frappe from frappe.utils.nestedset import get_root_of from erpnext.shopping_cart.cart import get_debtors_account -from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( get_shopping_cart_settings, ) diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index 6a923ae423..227881ac2a 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -180,7 +180,7 @@ $.extend(shopping_cart, { show_cart_navbar: function () { frappe.call({ - method: "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.is_cart_enabled", + method: "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.is_cart_enabled", callback: function(r) { $(".shopping-cart").toggleClass('hidden', r.message ? false : true); } diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 99c43bfc87..e9644cc722 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -274,7 +274,7 @@ def _make_customer(source_name, ignore_permissions=False): customer = frappe.get_doc(customer_doclist) customer.flags.ignore_permissions = ignore_permissions if quotation.get("party_name") == "Shopping Cart": - customer.customer_group = frappe.db.get_value("Shopping Cart Settings", None, + customer.customer_group = frappe.db.get_value("E Commerce Settings", None, "default_customer_group") try: diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 2f03ee7183..3965d82751 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -120,9 +120,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): 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.theme or "Light" - values[f"slide_{index + 1}_content_align"] = slide.content_align or "Centre" - values[f"slide_{index + 1}_primary_action_label"] = slide.label + 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 context.slideshow = values @@ -175,7 +174,7 @@ def get_product_list_for_group(product_group=None, start=0, limit=10, search=Non data = frappe.db.sql(query, {"product_group": product_group,"search": search, "today": nowdate()}, as_dict=1) data = adjust_qty_for_expired_items(data) - if cint(frappe.db.get_single_value("Shopping Cart Settings", "enabled")): + if cint(frappe.db.get_single_value("E Commerce Settings", "enabled")): for item in data: set_product_info_for_website(item) diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index bea3906eba..be94994440 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -33,7 +33,7 @@ def create_fiscal_year_and_company(args): def enable_shopping_cart(args): # Needs price_lists frappe.get_doc({ - "doctype": "Shopping Cart Settings", + "doctype": "E Commerce Settings", "enabled": 1, 'company': args.get('company_name') , 'price_list': frappe.db.get_value("Price List", {"selling": 1}), diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index c473395a9a..fbfcb102dd 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -534,7 +534,7 @@ def create_bank_account(args): pass def update_shopping_cart_settings(args): - shopping_cart = frappe.get_doc("Shopping Cart Settings") + shopping_cart = frappe.get_doc("E Commerce Settings") shopping_cart.update({ "enabled": 1, 'company': args.company_name, diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 3f1dfde016..cd9f1e8767 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -1,8 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - import frappe import frappe.defaults from frappe import _, throw @@ -11,10 +9,8 @@ from frappe.contacts.doctype.contact.contact import get_contact_name from frappe.utils import cint, cstr, flt, get_fullname from frappe.utils.nestedset import get_root_of +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings from erpnext.accounts.utils import get_account_name -from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( - get_shopping_cart_settings, -) from erpnext.utilities.product import get_qty_in_stock @@ -22,7 +18,7 @@ class WebsitePriceListMissingError(frappe.ValidationError): pass def set_cart_count(quotation=None): - if cint(frappe.db.get_singles_value("Shopping Cart Settings", "enabled")): + if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")): if not quotation: quotation = _get_cart_quotation() cart_count = cstr(len(quotation.get("items"))) @@ -49,7 +45,7 @@ def get_cart_quotation(doc=None): "shipping_addresses": get_shipping_addresses(party), "billing_addresses": get_billing_addresses(party), "shipping_rules": get_applicable_shipping_rules(party), - "cart_settings": frappe.get_cached_doc("Shopping Cart Settings") + "cart_settings": frappe.get_cached_doc("E Commerce Settings") } @frappe.whitelist() @@ -73,7 +69,7 @@ def get_billing_addresses(party=None): @frappe.whitelist() def place_order(): quotation = _get_cart_quotation() - cart_settings = frappe.db.get_value("Shopping Cart Settings", None, + cart_settings = frappe.db.get_value("E Commerce Settings", None, ["company", "allow_items_not_in_stock"], as_dict=1) quotation.company = cart_settings.company @@ -263,7 +259,7 @@ def guess_territory(): territory = frappe.db.get_value("Territory", geoip_country) return territory or \ - frappe.db.get_value("Shopping Cart Settings", None, "territory") or \ + frappe.db.get_value("E Commerce Settings", None, "territory") or \ get_root_of("Territory") def decorate_quotation_doc(doc): @@ -286,7 +282,7 @@ def _get_cart_quotation(party=None): if quotation: qdoc = frappe.get_doc("Quotation", quotation[0].name) else: - company = frappe.db.get_value("Shopping Cart Settings", None, ["company"]) + company = frappe.db.get_value("E Commerce Settings", None, ["company"]) qdoc = frappe.get_doc({ "doctype": "Quotation", "naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-", @@ -341,7 +337,7 @@ def apply_cart_settings(party=None, quotation=None): if not quotation: quotation = _get_cart_quotation(party) - cart_settings = frappe.get_doc("Shopping Cart Settings") + cart_settings = frappe.get_doc("E Commerce Settings") set_price_list_and_rate(quotation, cart_settings) @@ -418,7 +414,7 @@ def get_party(user=None): party_doctype = contact.links[0].link_doctype party = contact.links[0].link_name - cart_settings = frappe.get_doc("Shopping Cart Settings") + cart_settings = frappe.get_doc("E Commerce Settings") debtors_account = '' diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js deleted file mode 100644 index b38828e0d7..0000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -frappe.ui.form.on("Shopping Cart Settings", { - onload: function(frm) { - if(frm.doc.__onload && frm.doc.__onload.quotation_series) { - frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series; - frm.refresh_field("quotation_series"); - } - - frm.set_query('payment_gateway_account', function() { - return { 'filters': { 'payment_channel': "Email" } }; - }); - }, - refresh: function(frm) { - if (frm.doc.enabled) { - frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html( - `
${__("Follow these steps to create a landing page for your store")}: - - docs/store-landing-page - -
` - ); - } - }, - enabled: function(frm) { - if (frm.doc.enabled === 1) { - frm.set_value('enable_variants', 1); - } - else { - frm.set_value('company', ''); - frm.set_value('price_list', ''); - frm.set_value('default_customer_group', ''); - frm.set_value('quotation_series', ''); - } - } -}); diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json deleted file mode 100644 index 7a4bb20136..0000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "actions": [], - "creation": "2013-06-19 15:57:32", - "description": "Default settings for Shopping Cart", - "doctype": "DocType", - "document_type": "System", - "engine": "InnoDB", - "field_order": [ - "enabled", - "store_page_docs", - "display_settings", - "show_attachments", - "show_price", - "show_stock_availability", - "enable_variants", - "column_break_7", - "show_contact_us_button", - "show_quantity_in_website", - "show_apply_coupon_code_in_website", - "allow_items_not_in_stock", - "section_break_2", - "company", - "price_list", - "column_break_4", - "default_customer_group", - "quotation_series", - "section_break_8", - "enable_checkout", - "save_quotations_as_draft", - "column_break_11", - "payment_gateway_account", - "payment_success_url" - ], - "fields": [ - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Enable Shopping Cart" - }, - { - "fieldname": "display_settings", - "fieldtype": "Section Break", - "label": "Display Settings" - }, - { - "default": "0", - "fieldname": "show_attachments", - "fieldtype": "Check", - "label": "Show Public Attachments" - }, - { - "default": "0", - "fieldname": "show_price", - "fieldtype": "Check", - "label": "Show Price" - }, - { - "default": "0", - "fieldname": "show_stock_availability", - "fieldtype": "Check", - "label": "Show Stock Availability" - }, - { - "default": "0", - "fieldname": "show_contact_us_button", - "fieldtype": "Check", - "label": "Show Contact Us Button" - }, - { - "default": "0", - "depends_on": "show_stock_availability", - "fieldname": "show_quantity_in_website", - "fieldtype": "Check", - "label": "Show Stock Quantity" - }, - { - "default": "0", - "fieldname": "show_apply_coupon_code_in_website", - "fieldtype": "Check", - "label": "Show Apply Coupon Code" - }, - { - "default": "0", - "fieldname": "allow_items_not_in_stock", - "fieldtype": "Check", - "label": "Allow items not in stock to be added to cart" - }, - { - "depends_on": "enabled", - "fieldname": "section_break_2", - "fieldtype": "Section Break" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "mandatory_depends_on": "eval: doc.enabled === 1", - "options": "Company", - "remember_last_selected_value": 1 - }, - { - "description": "Prices will not be shown if Price List is not set", - "fieldname": "price_list", - "fieldtype": "Link", - "label": "Price List", - "mandatory_depends_on": "eval: doc.enabled === 1", - "options": "Price List" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "default_customer_group", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Customer Group", - "mandatory_depends_on": "eval: doc.enabled === 1", - "options": "Customer Group" - }, - { - "fieldname": "quotation_series", - "fieldtype": "Select", - "label": "Quotation Series", - "mandatory_depends_on": "eval: doc.enabled === 1" - }, - { - "collapsible": 1, - "collapsible_depends_on": "eval:doc.enable_checkout", - "depends_on": "enabled", - "fieldname": "section_break_8", - "fieldtype": "Section Break", - "label": "Checkout Settings" - }, - { - "default": "0", - "fieldname": "enable_checkout", - "fieldtype": "Check", - "label": "Enable Checkout" - }, - { - "default": "Orders", - "depends_on": "enable_checkout", - "description": "After payment completion redirect user to selected page.", - "fieldname": "payment_success_url", - "fieldtype": "Select", - "label": "Payment Success Url", - "mandatory_depends_on": "enable_checkout", - "options": "\nOrders\nInvoices\nMy Account" - }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, - { - "depends_on": "enable_checkout", - "fieldname": "payment_gateway_account", - "fieldtype": "Link", - "label": "Payment Gateway Account", - "mandatory_depends_on": "enable_checkout", - "options": "Payment Gateway Account" - }, - { - "fieldname": "column_break_7", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "enable_variants", - "fieldtype": "Check", - "label": "Enable Variants" - }, - { - "default": "0", - "depends_on": "eval: doc.enable_checkout == 0", - "fieldname": "save_quotations_as_draft", - "fieldtype": "Check", - "label": "Save Quotations as Draft" - }, - { - "depends_on": "doc.enabled", - "fieldname": "store_page_docs", - "fieldtype": "HTML" - } - ], - "icon": "fa fa-shopping-cart", - "idx": 1, - "issingle": 1, - "links": [], - "modified": "2021-03-02 17:34:57.642565", - "modified_by": "Administrator", - "module": "Shopping Cart", - "name": "Shopping Cart Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "Website Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "ASC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py deleted file mode 100644 index 8f4afda57e..0000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -# For license information, please see license.txt - -from __future__ import unicode_literals - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.utils import flt - - -class ShoppingCartSetupError(frappe.ValidationError): pass - -class ShoppingCartSettings(Document): - def onload(self): - self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series") - - def validate(self): - if self.enabled: - self.validate_price_list_exchange_rate() - - def validate_price_list_exchange_rate(self): - "Check if exchange rate exists for Price List currency (to Company's currency)." - from erpnext.setup.utils import get_exchange_rate - - if not self.enabled or not self.company or not self.price_list: - return # this function is also called from hooks, check values again - - company_currency = frappe.get_cached_value("Company", self.company, "default_currency") - price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency") - - if not company_currency: - msg = f"Please specify currency in Company {self.company}" - frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - - if not price_list_currency: - msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}" - frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - - if price_list_currency != company_currency: - from_currency, to_currency = price_list_currency, company_currency - - # Get exchange rate checks Currency Exchange Records too - exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling") - - if not flt(exchange_rate): - msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}" - frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError) - - def validate_tax_rule(self): - if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"): - frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError) - - def get_tax_master(self, billing_territory): - tax_master = self.get_name_from_territory(billing_territory, "sales_taxes_and_charges_masters", - "sales_taxes_and_charges_master") - return tax_master and tax_master[0] or None - - def get_shipping_rules(self, shipping_territory): - return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule") - -def validate_cart_settings(doc=None, method=None): - frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate") - -def get_shopping_cart_settings(): - if not getattr(frappe.local, "shopping_cart_settings", None): - frappe.local.shopping_cart_settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings") - - return frappe.local.shopping_cart_settings - -@frappe.whitelist(allow_guest=True) -def is_cart_enabled(): - return get_shopping_cart_settings().enabled - -def show_quantity_in_website(): - return get_shopping_cart_settings().show_quantity_in_website - -def check_shopping_cart_enabled(): - if not get_shopping_cart_settings().enabled: - frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError) - -def show_attachments(): - return get_shopping_cart_settings().show_attachments diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py deleted file mode 100644 index f8a22b0e02..0000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -# For license information, please see license.txt - -from __future__ import unicode_literals - -import unittest - -import frappe - -from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( - ShoppingCartSetupError, -) - - -class TestShoppingCartSettings(unittest.TestCase): - def setUp(self): - frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """) - - def get_cart_settings(self): - return frappe.get_doc({"doctype": "Shopping Cart Settings", - "company": "_Test Company"}) - - # NOTE: Exchangrate API has all enabled currencies that ERPNext supports. - # We aren't checking just currency exchange record anymore - # while validating price list currency exchange rate to that of company. - # The API is being used to fetch the rate which again almost always - # gives back a valid value (for valid currencies). - # This makes the test obsolete. - # Commenting because im not sure if there's a better test we can write - - # def test_exchange_rate_exists(self): - # frappe.db.sql("""delete from `tabCurrency Exchange`""") - - # cart_settings = self.get_cart_settings() - # cart_settings.price_list = "_Test Price List Rest of the World" - # self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate) - - # from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ - # currency_exchange_records - # frappe.get_doc(currency_exchange_records[0]).insert() - # cart_settings.validate_price_list_exchange_rate() - - def test_tax_rule_validation(self): - frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") - frappe.db.commit() - - cart_settings = self.get_cart_settings() - cart_settings.enabled = 1 - if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): - self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule) - - frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1") - -test_dependencies = ["Tax Rule"] diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/shopping_cart/product_info.py index fa6863696a..dd77536535 100644 --- a/erpnext/shopping_cart/product_info.py +++ b/erpnext/shopping_cart/product_info.py @@ -1,17 +1,14 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - import frappe from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list -from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( get_shopping_cart_settings, - show_quantity_in_website, + show_quantity_in_website ) -from erpnext.utilities.product import get_non_stock_item_status, get_price, get_qty_in_stock - +from erpnext.utilities.product import get_price, get_qty_in_stock, get_non_stock_item_status @frappe.whitelist(allow_guest=True) def get_product_info_for_website(item_code, skip_quotation_creation=False): diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index d105ab898b..140e1c6b38 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -10,26 +10,22 @@ class ProductQuery: """Query engine for product listing Attributes: - cart_settings (Document): Settings for Cart fields (list): Fields to fetch in query - filters (TYPE): Description - or_filters (list): Description + conditions (string): Conditions for query building + or_conditions (string): Search conditions page_length (Int): Length of page for the query settings (Document): E Commerce Settings DocType - filters (list) - or_filters (list) """ def __init__(self): self.settings = frappe.get_doc("E Commerce Settings") - self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.page_length = self.settings.products_per_page or 20 - self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', - 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage'] - self.filters = [] - self.or_filters = [['show_in_website', '=', 1]] - if not self.settings.get('hide_variants'): - self.or_filters.append(['show_variant_in_website', '=', 1]) + self.fields = ['wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of', + 'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description', + 'wi.route'] + self.conditions = "" + self.or_conditions = "" + self.substitutions = [] def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """Summary @@ -57,51 +53,14 @@ class ProductQuery: filters=[["Website Item Group", "item_group", "=", item_group]] ) + self.query_fields = (", ").join(self.fields) if attributes: - all_items = [] - for attribute, values in attributes.items(): - if not isinstance(values, list): - values = [values] - - items = frappe.get_all( - "Item", - fields=self.fields, - filters=[ - *self.filters, - ["Item Variant Attribute", "attribute", "=", attribute], - ["Item Variant Attribute", "attribute_value", "in", values], - ], - or_filters=self.or_filters, - start=start, - limit=self.page_length, - order_by="weightage desc" - ) - - items_dict = {item.name: item for item in items} - - all_items.append(set(items_dict.keys())) - - result = [items_dict.get(item) for item in list(set.intersection(*all_items))] + result = self.query_items_with_attributes(attributes, start) else: - result = frappe.get_all( - "Item", - fields=self.fields, - filters=self.filters, - or_filters=self.or_filters, - start=start, - limit=self.page_length, - order_by="weightage desc" - ) - - # Combine results having context of website item groups into item results - if item_group and website_item_groups: - items_list = {row.name for row in result} - for row in website_item_groups: - if row.wig_parent not in items_list: - result.append(row) - - result = sorted(result, key=lambda x: x.get("weightage"), reverse=True) + result = self.query_items(self.conditions, self.or_conditions, + self.substitutions, start=start) + # add price 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: @@ -109,6 +68,51 @@ class ProductQuery: return result + def query_items(self, conditions, or_conditions, substitutions, start=0): + """Build a query to fetch Website Items based on field filters.""" + return frappe.db.sql(""" + select distinct {query_fields} + from + `tabWebsite Item` wi, `tabItem Variant Attribute` iva + where + wi.published = 1 + {conditions} + {or_conditions} + limit {limit} offset {start} + """.format( + query_fields=self.query_fields, + conditions=conditions, + or_conditions=or_conditions, + limit=self.page_length, + start=start), + 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.""" + all_items = [] + self.conditions += " and iva.parent = wi.item_code" + + for attribute, values in attributes.items(): + if not isinstance(values, list): values = [values] + + conditions_copy = self.conditions + substitutions_copy = self.substitutions.copy() + + conditions_copy += " and iva.attribute = '{0}' and iva.attribute_value in ({1})" \ + .format(attribute, (", ").join(['%s'] * len(values))) + substitutions_copy.extend(values) + + items = self.query_items(conditions_copy, self.or_conditions, substitutions_copy, start=start) + + items_dict = {item.name: item for item in items} + # TODO: Replace Variants by their parent templates + + all_items.append(set(items_dict.keys())) + + result = [items_dict.get(item) for item in list(set.intersection(*all_items))] + return result + def build_fields_filters(self, filters): """Build filters for field values @@ -130,10 +134,11 @@ class ProductQuery: self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) elif isinstance(values, list): # If value is a list use `IN` query - self.filters.append([field, 'IN', values]) + self.conditions += " and wi.{0} in ({1})".format(field, (', ').join(['%s'] * len(values))) + self.substitutions.extend(values) else: # `=` will be faster than `IN` for most cases - self.filters.append([field, '=', values]) + self.conditions += " and wi.{0} = '{1}'".format(field, values) def build_search_filters(self, search_term): """Query search term in specified fields @@ -158,4 +163,5 @@ class ProductQuery: # Build or filters for query search = '%{}%'.format(search_term) - self.or_filters += [[field, 'like', search] for field in search_fields] + for field in search_fields: + self.or_conditions += " or {0} like '{1}'".format(field, search) diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/shopping_cart/test_shopping_cart.py index d1284cdf00..63166c6ab7 100644 --- a/erpnext/shopping_cart/test_shopping_cart.py +++ b/erpnext/shopping_cart/test_shopping_cart.py @@ -167,7 +167,7 @@ class TestShoppingCart(unittest.TestCase): # helper functions def enable_shopping_cart(self): - settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings") + settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings") settings.update({ "enabled": 1, @@ -197,7 +197,7 @@ class TestShoppingCart(unittest.TestCase): frappe.local.shopping_cart_settings = None def disable_shopping_cart(self): - settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings") + settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings") settings.enabled = 0 settings.save() frappe.local.shopping_cart_settings = None diff --git a/erpnext/shopping_cart/utils.py b/erpnext/shopping_cart/utils.py index f412e61f06..98b522994a 100644 --- a/erpnext/shopping_cart/utils.py +++ b/erpnext/shopping_cart/utils.py @@ -1,14 +1,9 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - import frappe -from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( - is_cart_enabled, -) - +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled def show_cart_count(): if (is_cart_enabled() and diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8cc9f74a42..2de4689f27 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -21,7 +21,6 @@ from frappe.utils import ( strip, ) from frappe.utils.html_utils import clean_html -from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow from frappe.website.utils import clear_cache from frappe.website.website_generator import WebsiteGenerator @@ -131,8 +130,6 @@ class Item(WebsiteGenerator): self.validate_attributes() self.validate_variant_attributes() self.validate_variant_based_on_change() - self.validate_website_image() - self.make_thumbnail() self.validate_fixed_asset() self.validate_retain_sample() self.validate_uom_conversion_factor() @@ -141,7 +138,6 @@ class Item(WebsiteGenerator): self.validate_item_defaults() self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() - self.update_show_in_website() self.validate_item_tax_net_rate_range() set_item_tax_from_hsn_code(self) @@ -156,7 +152,6 @@ class Item(WebsiteGenerator): self.validate_name_with_item_group() self.update_variants() self.update_item_price() - self.update_template_item() def validate_description(self): '''Clean HTML description if set''' @@ -218,95 +213,6 @@ class Item(WebsiteGenerator): stock_entry.add_comment("Comment", _("Opening Stock")) - def make_route(self): - if not self.route: - return cstr(frappe.db.get_value('Item Group', self.item_group, - 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5)) - - def validate_website_image(self): - if frappe.flags.in_import: - return - - """Validate if the website image is a public file""" - auto_set_website_image = False - if not self.website_image and self.image: - auto_set_website_image = True - self.website_image = self.image - - if not self.website_image: - return - - # find if website image url exists as public - file_doc = frappe.get_all("File", filters={ - "file_url": self.website_image - }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1) - - if file_doc: - file_doc = file_doc[0] - - if not file_doc: - if not auto_set_website_image: - frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name)) - - self.website_image = None - - elif file_doc.is_private: - if not auto_set_website_image: - frappe.msgprint(_("Website Image should be a public file or website URL")) - - self.website_image = None - - def make_thumbnail(self): - if frappe.flags.in_import: - return - - """Make a thumbnail of `website_image`""" - import requests.exceptions - - if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): - self.thumbnail = None - - if self.website_image and not self.thumbnail: - file_doc = None - - try: - file_doc = frappe.get_doc("File", { - "file_url": self.website_image, - "attached_to_doctype": "Item", - "attached_to_name": self.name - }) - except frappe.DoesNotExistError: - # cleanup - frappe.local.message_log.pop() - - except requests.exceptions.HTTPError: - frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image)) - self.website_image = None - - except requests.exceptions.SSLError: - frappe.msgprint( - _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)) - self.website_image = None - - # for CSV import - if self.website_image and not file_doc: - try: - file_doc = frappe.get_doc({ - "doctype": "File", - "file_url": self.website_image, - "attached_to_doctype": "Item", - "attached_to_name": self.name - }).save() - - except IOError: - self.website_image = None - - if file_doc: - if not file_doc.thumbnail_url: - file_doc.make_thumbnail() - - self.thumbnail = file_doc.thumbnail_url - def validate_fixed_asset(self): if self.is_fixed_asset: if self.is_stock_item: @@ -330,167 +236,6 @@ class Item(WebsiteGenerator): frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( self.item_code)) - def get_context(self, context): - context.show_search = True - context.search_link = '/product_search' - - context.parents = get_parent_item_groups(self.item_group) - context.body_class = "product-page" - - self.set_variant_context(context) - self.set_attribute_context(context) - self.set_disabled_attributes(context) - self.set_metatags(context) - self.set_shopping_cart_data(context) - - return context - - def set_variant_context(self, context): - if self.has_variants: - context.no_cache = True - - # load variants - # also used in set_attribute_context - context.variants = frappe.get_all("Item", - filters={"variant_of": self.name, "show_variant_in_website": 1}, - order_by="name asc") - - variant = frappe.form_dict.variant - if not variant and context.variants: - # the case when the item is opened for the first time from its list - variant = context.variants[0] - - if variant: - context.variant = frappe.get_doc("Item", variant) - - for fieldname in ("website_image", "website_image_alt", "web_long_description", "description", - "website_specifications"): - if context.variant.get(fieldname): - value = context.variant.get(fieldname) - if isinstance(value, list): - value = [d.as_dict() for d in value] - - context[fieldname] = value - - if self.slideshow: - if context.variant and context.variant.slideshow: - context.update(get_slideshow(context.variant)) - else: - context.update(get_slideshow(self)) - - def set_attribute_context(self, context): - if not self.has_variants: - return - - attribute_values_available = {} - context.attribute_values = {} - context.selected_attributes = {} - - # load attributes - for v in context.variants: - v.attributes = frappe.get_all("Item Variant Attribute", - fields=["attribute", "attribute_value"], - filters={"parent": v.name}) - # make a map for easier access in templates - v.attribute_map = frappe._dict({}) - for attr in v.attributes: - v.attribute_map[attr.attribute] = attr.attribute_value - - for attr in v.attributes: - values = attribute_values_available.setdefault(attr.attribute, []) - if attr.attribute_value not in values: - values.append(attr.attribute_value) - - if v.name == context.variant.name: - context.selected_attributes[attr.attribute] = attr.attribute_value - - # filter attributes, order based on attribute table - for attr in self.attributes: - values = context.attribute_values.setdefault(attr.attribute, []) - - if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): - for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): - values.append(val) - - else: - # get list of values defined (for sequence) - for attr_value in frappe.db.get_all("Item Attribute Value", - fields=["attribute_value"], - filters={"parent": attr.attribute}, order_by="idx asc"): - - if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): - values.append(attr_value.attribute_value) - - context.variant_info = json.dumps(context.variants) - - def set_disabled_attributes(self, context): - """Disable selection options of attribute combinations that do not result in a variant""" - if not self.attributes or not self.has_variants: - return - - context.disabled_attributes = {} - attributes = [attr.attribute for attr in self.attributes] - - def find_variant(combination): - for variant in context.variants: - if len(variant.attributes) < len(attributes): - continue - - if "combination" not in variant: - ref_combination = [] - - for attr in variant.attributes: - idx = attributes.index(attr.attribute) - ref_combination.insert(idx, attr.attribute_value) - - variant["combination"] = ref_combination - - if not (set(combination) - set(variant["combination"])): - # check if the combination is a subset of a variant combination - # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5] - return True - - for i, attr in enumerate(self.attributes): - if i == 0: - continue - - combination_source = [] - - # loop through previous attributes - for prev_attr in self.attributes[:i]: - combination_source.append([context.selected_attributes.get(prev_attr.attribute)]) - - combination_source.append(context.attribute_values[attr.attribute]) - - for combination in itertools.product(*combination_source): - if not find_variant(combination): - context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1]) - - def set_metatags(self, context): - context.metatags = frappe._dict({}) - - safe_description = frappe.utils.to_markdown(self.description) - - context.metatags.url = frappe.utils.get_url() + '/' + context.route - - if context.website_image: - if context.website_image.startswith('http'): - url = context.website_image - else: - url = frappe.utils.get_url() + context.website_image - context.metatags.image = url - - context.metatags.description = safe_description[:300] - - context.metatags.title = self.item_name or self.item_code - - context.metatags['og:type'] = 'product' - context.metatags['og:site_name'] = 'ERPNext' - - def set_shopping_cart_data(self, context): - from erpnext.shopping_cart.product_info import get_product_info_for_website - context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True) - def add_default_uom_in_conversion_factor_table(self): uom_conv_list = [d.uom for d in self.get("uoms")] if self.stock_uom not in uom_conv_list: @@ -505,10 +250,6 @@ class Item(WebsiteGenerator): [self.remove(d) for d in to_remove] - def update_show_in_website(self): - if self.disabled: - self.show_in_website = False - def validate_item_tax_net_rate_range(self): for tax in self.get('taxes'): if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate): @@ -678,7 +419,7 @@ class Item(WebsiteGenerator): if merge: self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name) - if self.route: + if self.published_in_website: invalidate_cache_for_item(self) clear_cache(self.route) @@ -777,25 +518,6 @@ class Item(WebsiteGenerator): where item_code = %s and docstatus < 2 """, (self.description, self.name)) - def update_template_item(self): - """Set Show in Website for Template Item if True for its Variant""" - if not self.variant_of: - return - - if self.show_in_website: - self.show_variant_in_website = 1 - self.show_in_website = 0 - - if self.show_variant_in_website: - # show template - template_item = frappe.get_doc("Item", self.variant_of) - - if not template_item.show_in_website: - template_item.show_in_website = 1 - template_item.flags.dont_update_variants = True - template_item.flags.ignore_permissions = True - template_item.save() - def validate_item_defaults(self): companies = {row.company for row in self.item_defaults} @@ -1065,7 +787,6 @@ class Item(WebsiteGenerator): 'item_code': item, 'item_name': item, 'description': item, - 'show_in_website': 1, 'is_sales_item': 1, 'is_purchase_item': 1, 'is_stock_item': 1, diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html index 39a30d0d7c..e9b0916a70 100644 --- a/erpnext/templates/generators/item/item_image.html +++ b/erpnext/templates/generators/item/item_image.html @@ -23,7 +23,7 @@ }) {% else %} - {{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }} + {{ product_image(doc.website_image or doc.image or 'no-image.jpg', alt=doc.website_image_alt or doc.item_name) }} {% endif %} diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 4482bc10cf..979298fa8d 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -4,7 +4,7 @@ {% set select_address = True %} {% endif %} -{% set show_coupon_code = frappe.db.get_single_value('Shopping Cart Settings', 'show_apply_coupon_code_in_website') %} +{% set show_coupon_code = frappe.db.get_single_value('E Commerce Settings', 'show_apply_coupon_code_in_website') %} {% if show_coupon_code == 1%}
diff --git a/erpnext/templates/includes/products_as_list.html b/erpnext/templates/includes/products_as_list.html index 9bf9fd95d7..976d6147bc 100644 --- a/erpnext/templates/includes/products_as_list.html +++ b/erpnext/templates/includes/products_as_list.html @@ -1,4 +1,4 @@ -{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %} +{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body, product_image_square %}
diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index d4e81ab096..59df433804 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -6,10 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( - show_attachments, -) - +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import show_attachments def get_context(context): context.no_cache = 1 @@ -26,7 +23,7 @@ def get_context(context): context.payment_ref = frappe.db.get_value("Payment Request", {"reference_name": frappe.form_dict.name}, "name") - context.enabled_checkout = frappe.get_doc("Shopping Cart Settings").enable_checkout + context.enabled_checkout = frappe.get_doc("E Commerce Settings").enable_checkout default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value") if default_print_format: diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 1c641b59ad..37e07f4c7e 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -129,7 +129,7 @@ $(() => { Object.assign(field_filters, { item_group }); } return new Promise((resolve, reject) => { - frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args) + frappe.call('erpnext.www.all-products.index.get_products_html_for_website', args) .then(r => { if (r.exc) reject(r.exc); else resolve(r.message); diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py index 67d51ca161..688c02935a 100644 --- a/erpnext/www/all-products/index.py +++ b/erpnext/www/all-products/index.py @@ -1,9 +1,7 @@ import frappe - -from erpnext.portal.product_configurator.utils import (get_products_for_website, get_e_commerce_settings, - get_field_filter_data, get_attribute_filter_data) -from erpnext.shopping_cart.filters import ProductFiltersBuilder +from frappe.utils import cint from erpnext.shopping_cart.product_query import ProductQuery +from erpnext.shopping_cart.filters import ProductFiltersBuilder sitemap = 1 @@ -13,7 +11,7 @@ def get_context(context): search = frappe.form_dict.search field_filters = frappe.parse_json(frappe.form_dict.field_filters) attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters) - start = frappe.parse_json(frappe.form_dict.start) + start = cint(frappe.parse_json(frappe.form_dict.start)) else: search = field_filters = attribute_filters = None start = 0 @@ -24,15 +22,34 @@ def get_context(context): # Add homepage as parent context.parents = [{"name": frappe._("Home"), "route":"/"}] - e_commerce_settings = get_e_commerce_settings() filter_engine = ProductFiltersBuilder() context.field_filters = filter_engine.get_field_filters() context.attribute_filters = filter_engine.get_attribute_filters() - context.e_commerce_settings = e_commerce_settings + context.e_commerce_settings = engine.settings context.body_class = "product-page" - context.page_length = e_commerce_settings.products_per_page or 20 + context.page_length = engine.settings.products_per_page or 20 context.no_cache = 1 - print(context) + +@frappe.whitelist(allow_guest=True) +def get_products_html_for_website(field_filters=None, attribute_filters=None): + """Get Products on filter change.""" + field_filters = frappe.parse_json(field_filters) + attribute_filters = frappe.parse_json(attribute_filters) + + engine = ProductQuery() + items = 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 + })) + html = ''.join(item_html) + + if not items: + html = frappe.render_template('erpnext/www/all-products/not_found.html', {}) + + return html From 025574d9b57bebbe63a3604b84b84d3cc42d1f23 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 19 Feb 2021 12:50:01 +0530 Subject: [PATCH 003/225] chore: Removing Item's Website section & references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed old onboarding slide json and `create_onboarding_docs` methods - Removed website sections from Item master - Removed references to item website fields - Shifted Item doctype website methods to Website Item - Removed WebsiteGenerator from Item doctype - Website Items in Homepage Products section - Removed redundant code from item_group.py - Fix: Item field filters won’t appear in website --- .../doctype/coupon_code/test_coupon_code.py | 2 - erpnext/buying/doctype/supplier/supplier.py | 22 - .../add_a_few_suppliers.json | 49 -- erpnext/controllers/item_variant.py | 5 +- erpnext/demo/data/drug_list.json | 63 --- .../doctype/website_item/website_item.js | 15 +- .../doctype/website_item/website_item.json | 5 +- .../doctype/website_item/website_item.py | 42 +- erpnext/hooks.py | 2 +- erpnext/portal/doctype/homepage/homepage.js | 9 +- erpnext/portal/doctype/homepage/homepage.json | 439 ++---------------- erpnext/portal/doctype/homepage/homepage.py | 2 +- .../homepage_featured_product.json | 10 +- .../website_attribute/website_attribute.json | 98 ++-- erpnext/selling/doctype/customer/customer.py | 24 - .../add_a_few_customers.json | 49 -- .../setup/doctype/item_group/item_group.py | 87 ---- .../welcome_back_to_erpnext!.json | 23 - .../welcome_to_erpnext!.json | 23 - erpnext/shopping_cart/filters.py | 3 +- erpnext/shopping_cart/search.py | 2 +- erpnext/stock/doctype/item/item.js | 23 +- erpnext/stock/doctype/item/item.json | 147 ------ erpnext/stock/doctype/item/item.py | 80 +--- erpnext/stock/doctype/item/test_records.json | 13 +- .../item_variant_settings.js | 5 +- .../item_variant_settings.py | 6 +- .../add_a_few_products_you_buy_or_sell.json | 52 --- 28 files changed, 139 insertions(+), 1161 deletions(-) delete mode 100644 erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json delete mode 100644 erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json delete mode 100644 erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json delete mode 100644 erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json delete mode 100644 erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py index bf8c014a5b..b27f4eb503 100644 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py @@ -41,9 +41,7 @@ def test_create_test_data(): "selling_cost_center": "Main - _TC", "income_account": "Sales - _TC" }], - "show_in_website": 1, "route":"-test-tesla-car", - "website_warehouse": "Stores - _TC" }) item.insert() # create test item price diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 0ab01712e3..1a34be063b 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -128,28 +128,6 @@ class Supplier(TransactionBase): if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name': frappe.db.set(self, "supplier_name", newdn) - def create_onboarding_docs(self, args): - company = frappe.defaults.get_defaults().get('company') or \ - frappe.db.get_single_value('Global Defaults', 'default_company') - - for i in range(1, args.get('max_count')): - supplier = args.get('supplier_name_' + str(i)) - if supplier: - try: - doc = frappe.get_doc({ - 'doctype': self.doctype, - 'supplier_name': supplier, - 'supplier_group': _('Local'), - 'company': company - }).insert() - - if args.get('supplier_email_' + str(i)): - from erpnext.selling.doctype.customer.customer import create_contact - create_contact(supplier, 'Supplier', - doc.name, args.get('supplier_email_' + str(i))) - except frappe.NameError: - pass - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json b/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json deleted file mode 100644 index ce3d8cfb7b..0000000000 --- a/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "add_more_button": 1, - "app": "ERPNext", - "creation": "2019-11-15 14:45:32.626641", - "docstatus": 0, - "doctype": "Onboarding Slide", - "domains": [], - "help_links": [ - { - "label": "Learn More", - "video_id": "zsrrVDk6VBs" - } - ], - "idx": 0, - "image_src": "", - "is_completed": 0, - "max_count": 3, - "modified": "2019-12-09 17:54:18.452038", - "modified_by": "Administrator", - "name": "Add A Few Suppliers", - "owner": "Administrator", - "ref_doctype": "Supplier", - "slide_desc": "", - "slide_fields": [ - { - "align": "", - "fieldname": "supplier_name", - "fieldtype": "Data", - "label": "Supplier Name", - "placeholder": "", - "reqd": 1 - }, - { - "align": "", - "fieldtype": "Column Break", - "reqd": 0 - }, - { - "align": "", - "fieldname": "supplier_email", - "fieldtype": "Data", - "label": "Supplier Email", - "reqd": 1 - } - ], - "slide_order": 50, - "slide_title": "Add A Few Suppliers", - "slide_type": "Create" -} \ No newline at end of file diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 1b56ae9d4d..6935fd7ab4 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -264,9 +264,8 @@ def generate_keyed_value_combinations(args): def copy_attributes_to_variant(item, variant): # copy non no-copy fields - exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website", - "show_variant_in_website", "opening_stock", "variant_of", "valuation_rate", - "has_variants", "attributes"] + exclude_fields = ["naming_series", "item_code", "item_name", "published_in_website", + "opening_stock", "variant_of", "valuation_rate"] if item.variant_based_on=='Manufacturer': # don't copy manufacturer values if based on part no diff --git a/erpnext/demo/data/drug_list.json b/erpnext/demo/data/drug_list.json index e91c30d199..c7c06c946d 100644 --- a/erpnext/demo/data/drug_list.json +++ b/erpnext/demo/data/drug_list.json @@ -54,7 +54,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -138,7 +137,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -220,7 +218,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -302,7 +299,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -384,7 +380,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -466,7 +461,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -548,7 +542,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -630,7 +623,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -712,7 +704,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -794,7 +785,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -876,7 +866,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -958,7 +947,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1040,7 +1028,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1122,7 +1109,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1204,7 +1190,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1286,7 +1271,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1368,7 +1352,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1450,7 +1433,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1532,7 +1514,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1614,7 +1595,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1696,7 +1676,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1778,7 +1757,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1860,7 +1838,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -1942,7 +1919,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2024,7 +2000,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2106,7 +2081,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2188,7 +2162,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2270,7 +2243,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2352,7 +2324,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2434,7 +2405,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2516,7 +2486,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2598,7 +2567,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2680,7 +2648,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2762,7 +2729,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2844,7 +2810,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -2926,7 +2891,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3008,7 +2972,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3092,7 +3055,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3174,7 +3136,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3256,7 +3217,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3338,7 +3298,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3420,7 +3379,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3502,7 +3460,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3584,7 +3541,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3666,7 +3622,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3748,7 +3703,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3830,7 +3784,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3912,7 +3865,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -3994,7 +3946,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4076,7 +4027,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4158,7 +4108,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4240,7 +4189,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4322,7 +4270,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4404,7 +4351,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4486,7 +4432,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4568,7 +4513,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4650,7 +4594,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4732,7 +4675,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4814,7 +4756,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4896,7 +4837,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -4978,7 +4918,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -5060,7 +4999,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, @@ -5142,7 +5080,6 @@ "safety_stock": 0.0, "selling_cost_center": null, "serial_no_series": null, - "show_in_website": 0, "show_variant_in_website": 0, "slideshow": null, "standard_rate": 0.0, diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js index ecea74bc78..0d8a51e1de 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.js +++ b/erpnext/e_commerce/doctype/website_item/website_item.js @@ -2,7 +2,18 @@ // For license information, please see license.txt frappe.ui.form.on('Website Item', { - // refresh: function(frm) { + image: function() { + refresh_field("image_view"); + }, - // } + copy_from_item_group: function(frm) { + return frm.call({ + doc: frm.doc, + method: "copy_specification_from_item_group" + }); + }, + + set_meta_tags(frm) { + frappe.utils.set_meta_tag(frm.doc.route); + } }); diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index 02717eae14..3a2990639b 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -271,7 +271,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-12 16:49:42.275517", + "modified": "2021-02-18 13:23:18.286883", "modified_by": "Administrator", "module": "E-commerce", "name": "Website Item", @@ -290,9 +290,10 @@ "write": 1 } ], + "search_fields": "item_code, item_name ,item_group", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "web_item_name", + "title_field": "item_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 55436f257d..51983ffd27 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -7,15 +7,12 @@ import frappe import json import itertools from frappe import _ -from six import iteritems -from frappe.website.doctype.website_slideshow.website_slideshow import \ - get_slideshow from frappe.website.render import clear_cache from frappe.website.website_generator import WebsiteGenerator -from frappe.utils import cstr, random_string, cint +from frappe.utils import cstr, random_string, cint, flt -from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups +from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) class WebsiteItem(WebsiteGenerator): website = frappe._dict( @@ -39,7 +36,13 @@ class WebsiteItem(WebsiteGenerator): self.make_thumbnail() self.publish_unpublish_desk_item(publish=True) + if not self.get("__islocal"): + self.old_website_item_groups = frappe.db.sql_list("""select item_group + from `tabWebsite Item Group` + where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name) + def on_update(self): + invalidate_cache_for_web_item(self) self.update_template_item() def on_trash(self): @@ -69,10 +72,9 @@ class WebsiteItem(WebsiteGenerator): # show template template_item = frappe.get_doc("Item", self.variant_of) - if not template_item.published: - template_item.published = 1 + if not template_item.published_in_website: template_item.flags.ignore_permissions = True - template_item.save() + make_website_item(template_item) def validate_website_image(self): if frappe.flags.in_import: @@ -160,7 +162,6 @@ class WebsiteItem(WebsiteGenerator): self.thumbnail = file_doc.thumbnail_url def get_context(self, context): - print(context) context.show_search = True context.search_link = '/search' @@ -174,7 +175,6 @@ class WebsiteItem(WebsiteGenerator): self.set_disabled_attributes(context) self.set_metatags(context) self.set_shopping_cart_data(context) - print("IN WEB ITEM") return context @@ -317,6 +317,28 @@ class WebsiteItem(WebsiteGenerator): from erpnext.shopping_cart.product_info import get_product_info_for_website context.shopping_cart = get_product_info_for_website(self.item_code, skip_quotation_creation=True) + def copy_specification_from_item_group(self): + self.set("website_specifications", []) + if self.item_group: + for label, desc in frappe.db.get_values("Item Website Specification", + {"parent": self.item_group}, ["label", "description"]): + row = self.append("website_specifications") + row.label = label + row.description = desc + +def invalidate_cache_for_web_item(doc): + from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website + + invalidate_cache_for(doc, doc.item_group) + + website_item_groups = list(set((doc.get("old_website_item_groups") or []) + + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group])) + + for item_group in website_item_groups: + invalidate_cache_for(doc, item_group) + + invalidate_item_variants_cache_for_website(doc) + @frappe.whitelist() def make_website_item(doc): if not doc: diff --git a/erpnext/hooks.py b/erpnext/hooks.py index d0142d3d0d..735103678e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -77,7 +77,7 @@ domains = { 'Services': 'erpnext.domains.services', } -website_generators = ["Item Group", "Item", "BOM", "Sales Partner", +website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner", "Job Opening", "Student Admission"] website_context = { diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js index c7c66e0055..ea34bb17e5 100644 --- a/erpnext/portal/doctype/homepage/homepage.js +++ b/erpnext/portal/doctype/homepage/homepage.js @@ -3,9 +3,9 @@ frappe.ui.form.on('Homepage', { setup: function(frm) { - frm.fields_dict["products"].grid.get_field("item_code").get_query = function(){ + frm.fields_dict["products"].grid.get_field("item").get_query = function(){ return { - filters: {'show_in_website': 1} + filters: {'published': 1} } } }, @@ -21,11 +21,10 @@ frappe.ui.form.on('Homepage', { }); frappe.ui.form.on('Homepage Featured Product', { - view: function(frm, cdt, cdn){ var child= locals[cdt][cdn] - if(child.item_code && frm.doc.products_url){ - window.location.href = frm.doc.products_url + '/' + encodeURIComponent(child.item_code); + if(child.item_code && child.route){ + window.open('/' + child.route, '_blank'); } } }); diff --git a/erpnext/portal/doctype/homepage/homepage.json b/erpnext/portal/doctype/homepage/homepage.json index ad27278dc6..73f816d4d4 100644 --- a/erpnext/portal/doctype/homepage/homepage.json +++ b/erpnext/portal/doctype/homepage/homepage.json @@ -1,518 +1,143 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", + "actions": [], "beta": 1, "creation": "2016-04-22 05:27:52.109319", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "company", + "hero_section_based_on", + "column_break_2", + "title", + "section_break_4", + "tag_line", + "description", + "hero_image", + "slideshow", + "hero_section", + "products_section", + "products_url", + "products" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "company", "fieldtype": "Link", - "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": "Company", - "length": 0, - "no_copy": 0, "options": "Company", - "permlevel": 0, - "precision": "", - "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": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "hero_section_based_on", "fieldtype": "Select", - "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": "Hero Section Based On", - "length": 0, - "no_copy": 0, - "options": "Default\nSlideshow\nHomepage Section", - "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": "Default\nSlideshow\nHomepage Section" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "fieldtype": "Column 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, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "fieldname": "title", "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": "Title", - "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": "Title" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "fieldname": "section_break_4", "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": "Hero Section", - "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": "Hero Section" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.hero_section_based_on === 'Default'", "description": "Company Tagline for website homepage", "fieldname": "tag_line", "fieldtype": "Data", - "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": "Tag Line", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.hero_section_based_on === 'Default'", "description": "Company Description for website homepage", "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, - "permlevel": 0, - "precision": "", - "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": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.hero_section_based_on === 'Default'", "fieldname": "hero_image", "fieldtype": "Attach Image", - "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": "Hero Image", - "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": "Hero Image" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.hero_section_based_on === 'Slideshow'", - "description": "", "fieldname": "slideshow", "fieldtype": "Link", - "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": "Homepage Slideshow", - "length": 0, - "no_copy": 0, - "options": "Website Slideshow", - "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": "Website Slideshow" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.hero_section_based_on === 'Homepage Section'", "fieldname": "hero_section", "fieldtype": "Link", - "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": "Homepage Section", - "length": 0, - "no_copy": 0, - "options": "Homepage Section", - "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": "Homepage Section" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "fieldname": "products_section", "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": "Products", - "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": "Products" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "/products", + "default": "/all-products", "fieldname": "products_url", "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": "URL for \"All Products\"", - "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": "URL for \"All Products\"" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Products to be shown on website homepage", "fieldname": "products", "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": "Products", - "length": 0, - "no_copy": 0, "options": "Homepage Featured Product", - "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, "width": "40px" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-02 23:12:59.676202", + "links": [], + "modified": "2021-02-18 13:29:29.531639", "modified_by": "Administrator", "module": "Portal", "name": "Homepage", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, - "report": 0, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, - "report": 0, "role": "Administrator", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", "title_field": "company", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py index 7eeaf4b80a..4477011817 100644 --- a/erpnext/portal/doctype/homepage/homepage.py +++ b/erpnext/portal/doctype/homepage/homepage.py @@ -17,7 +17,7 @@ class Homepage(Document): def setup_items(self): for d in frappe.get_all('Item', fields=['name', 'item_name', 'description', 'image'], - filters={'show_in_website': 1}, limit=3): + filters={'published_in_website': 1}, limit=3): doc = frappe.get_doc('Item', d.name) if not doc.route: diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json index 01c32efec9..63789e35b5 100644 --- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json +++ b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json @@ -25,10 +25,10 @@ "fieldtype": "Link", "in_filter": 1, "in_list_view": 1, - "label": "Item Code", + "label": "Item", "oldfieldname": "item_code", "oldfieldtype": "Link", - "options": "Item", + "options": "Website Item", "print_width": "150px", "reqd": 1, "search_index": 1, @@ -63,7 +63,7 @@ "collapsible": 1, "fieldname": "section_break_5", "fieldtype": "Section Break", - "label": "Description" + "label": "Details" }, { "fetch_from": "item_code.web_long_description", @@ -89,12 +89,14 @@ "label": "Image" }, { + "fetch_from": "item_code.thumbnail", "fieldname": "thumbnail", "fieldtype": "Attach Image", "hidden": 1, "label": "Thumbnail" }, { + "fetch_from": "item_code.route", "fieldname": "route", "fieldtype": "Small Text", "label": "route", @@ -104,7 +106,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-25 15:27:49.573537", + "modified": "2021-02-18 13:05:50.669311", "modified_by": "Administrator", "module": "Portal", "name": "Homepage Featured Product", diff --git a/erpnext/portal/doctype/website_attribute/website_attribute.json b/erpnext/portal/doctype/website_attribute/website_attribute.json index 2874dc432c..eed33ec10e 100644 --- a/erpnext/portal/doctype/website_attribute/website_attribute.json +++ b/erpnext/portal/doctype/website_attribute/website_attribute.json @@ -1,76 +1,32 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2019-01-01 13:04:54.479079", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2019-01-01 13:04:54.479079", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "attribute" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "attribute", - "fieldtype": "Link", - "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": "Attribute", - "length": 0, - "no_copy": 0, - "options": "Item Attribute", - "permlevel": 0, - "precision": "", - "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": 0 + "fieldname": "attribute", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Attribute", + "options": "Item Attribute", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-01-01 13:04:59.715572", - "modified_by": "Administrator", - "module": "Portal", - "name": "Website Attribute", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2021-02-18 13:18:57.810536", + "modified_by": "Administrator", + "module": "Portal", + "name": "Website Attribute", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 7adf2cd909..3f60801c6e 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -294,30 +294,6 @@ class Customer(TransactionBase): .format(frappe.bold(self.customer_name)) ) - def create_onboarding_docs(self, args): - defaults = frappe.defaults.get_defaults() - company = defaults.get('company') or \ - frappe.db.get_single_value('Global Defaults', 'default_company') - - for i in range(1, args.get('max_count')): - customer = args.get('customer_name_' + str(i)) - if customer: - try: - doc = frappe.get_doc({ - 'doctype': self.doctype, - 'customer_name': customer, - 'customer_type': 'Company', - 'customer_group': _('Commercial'), - 'territory': defaults.get('country'), - 'company': company - }).insert() - - if args.get('customer_email_' + str(i)): - create_contact(customer, self.doctype, - doc.name, args.get("customer_email_" + str(i))) - except frappe.NameError: - pass - def create_contact(contact, party_type, party, email): """Create contact based on given contact name""" contact = contact.split(' ') diff --git a/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json deleted file mode 100644 index 92d00bcb38..0000000000 --- a/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "add_more_button": 1, - "app": "ERPNext", - "creation": "2019-11-15 14:44:10.065014", - "docstatus": 0, - "doctype": "Onboarding Slide", - "domains": [], - "help_links": [ - { - "label": "Learn More", - "video_id": "zsrrVDk6VBs" - } - ], - "idx": 0, - "image_src": "", - "is_completed": 0, - "max_count": 3, - "modified": "2019-12-09 17:54:01.686006", - "modified_by": "Administrator", - "name": "Add A Few Customers", - "owner": "Administrator", - "ref_doctype": "Customer", - "slide_desc": "", - "slide_fields": [ - { - "align": "", - "fieldname": "customer_name", - "fieldtype": "Data", - "label": "Customer Name", - "placeholder": "", - "reqd": 1 - }, - { - "align": "", - "fieldtype": "Column Break", - "reqd": 0 - }, - { - "align": "", - "fieldname": "customer_email", - "fieldtype": "Data", - "label": "Email ID", - "reqd": 1 - } - ], - "slide_order": 40, - "slide_title": "Add A Few Customers", - "slide_type": "Create" -} \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 3965d82751..d19e1897fc 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -138,85 +138,6 @@ class ItemGroup(NestedSet, WebsiteGenerator): from erpnext.stock.doctype.item.item import validate_item_default_company_links validate_item_default_company_links(self.item_group_defaults) -@frappe.whitelist(allow_guest=True) -def get_product_list_for_group(product_group=None, start=0, limit=10, search=None): - if product_group: - item_group = frappe.get_cached_doc('Item Group', product_group) - if item_group.is_group: - # return child item groups if the type is of "Is Group" - return get_child_groups_for_list_in_html(item_group, start, limit, search) - - child_groups = ", ".join(frappe.db.escape(i[0]) for i in get_child_groups(product_group)) - - # base query - query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group, - I.description, I.web_long_description as website_description, I.is_stock_item, - case when (S.actual_qty - S.reserved_qty) > 0 then 1 else 0 end as in_stock, I.website_warehouse, - I.has_batch_no - from `tabItem` I - left join tabBin S on I.item_code = S.item_code and I.website_warehouse = S.warehouse - where I.show_in_website = 1 - and I.disabled = 0 - and (I.end_of_life is null or I.end_of_life='0000-00-00' or I.end_of_life > %(today)s) - and (I.variant_of = '' or I.variant_of is null) - and (I.item_group in ({child_groups}) - or I.name in (select parent from `tabWebsite Item Group` where item_group in ({child_groups}))) - """.format(child_groups=child_groups) - # search term condition - if search: - query += """ and (I.web_long_description like %(search)s - or I.item_name like %(search)s - or I.name like %(search)s)""" - search = "%" + cstr(search) + "%" - - query += """order by I.weightage desc, in_stock desc, I.modified desc limit %s, %s""" % (cint(start), cint(limit)) - - data = frappe.db.sql(query, {"product_group": product_group,"search": search, "today": nowdate()}, as_dict=1) - data = adjust_qty_for_expired_items(data) - - if cint(frappe.db.get_single_value("E Commerce Settings", "enabled")): - for item in data: - set_product_info_for_website(item) - - return data - -def get_child_groups_for_list_in_html(item_group, start, limit, search): - search_filters = None - if search_filters: - search_filters = [ - dict(name = ('like', '%{}%'.format(search))), - dict(description = ('like', '%{}%'.format(search))) - ] - data = frappe.db.get_all('Item Group', - fields = ['name', 'route', 'description', 'image'], - filters = dict( - show_in_website = 1, - parent_item_group = item_group.name, - lft = ('>', item_group.lft), - rgt = ('<', item_group.rgt), - ), - or_filters = search_filters, - order_by = 'weightage desc, name asc', - start = start, - limit = limit - ) - - return data - -def adjust_qty_for_expired_items(data): - adjusted_data = [] - - for item in data: - if item.get('has_batch_no') and item.get('website_warehouse'): - stock_qty_dict = get_qty_in_stock( - item.get('name'), 'website_warehouse', item.get('website_warehouse')) - qty = stock_qty_dict.stock_qty[0][0] if stock_qty_dict.stock_qty else 0 - item['in_stock'] = 1 if qty else 0 - adjusted_data.append(item) - - return adjusted_data - - def get_child_groups(item_group_name): item_group = frappe.get_doc("Item Group", item_group_name) return frappe.db.sql("""select name @@ -245,14 +166,6 @@ def get_item_for_list_in_html(context): return frappe.get_template(products_template).render(context) -def get_group_item_count(item_group): - child_groups = ", ".join('"' + i[0] + '"' for i in get_child_groups(item_group)) - return frappe.db.sql("""select count(*) from `tabItem` - where docstatus = 0 and show_in_website = 1 - and (item_group in (%s) - or name in (select parent from `tabWebsite Item Group` - where item_group in (%s))) """ % (child_groups, child_groups))[0][0] - def get_parent_item_groups(item_group_name): base_parents = [ diff --git a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json deleted file mode 100644 index f00dc947d2..0000000000 --- a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "add_more_button": 0, - "app": "ERPNext", - "creation": "2019-12-04 19:21:39.995776", - "docstatus": 0, - "doctype": "Onboarding Slide", - "domains": [], - "help_links": [], - "idx": 0, - "image_src": "", - "is_completed": 0, - "max_count": 3, - "modified": "2019-12-09 17:53:53.849953", - "modified_by": "Administrator", - "name": "Welcome back to ERPNext!", - "owner": "Administrator", - "slide_desc": "

Let's continue where you left from!

", - "slide_fields": [], - "slide_module": "Setup", - "slide_order": 0, - "slide_title": "Welcome back to ERPNext!", - "slide_type": "Continue" -} \ No newline at end of file diff --git a/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json deleted file mode 100644 index 37eb67b1d7..0000000000 --- a/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "add_more_button": 0, - "app": "ERPNext", - "creation": "2019-11-26 17:01:26.671859", - "docstatus": 0, - "doctype": "Onboarding Slide", - "domains": [], - "help_links": [], - "idx": 0, - "image_src": "", - "is_completed": 0, - "max_count": 0, - "modified": "2019-12-22 21:26:28.414597", - "modified_by": "Administrator", - "name": "Welcome to ERPNext!", - "owner": "Administrator", - "slide_desc": "
Setting up an ERP can be overwhelming. But don't worry, we have got your back! This wizard will help you onboard to ERPNext in a short time!
", - "slide_fields": [], - "slide_module": "Setup", - "slide_order": 1, - "slide_title": "Welcome to ERPNext!", - "slide_type": "Information" -} \ No newline at end of file diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index ed32db2105..4fe346b659 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -48,8 +48,7 @@ class ProductFiltersBuilder: values = [d.name for d in frappe.get_all(doctype, filters)] # Remove None - if None in values: - values.remove(None) + if None in values: values.remove(None) if values: filter_data.append([df, values]) diff --git a/erpnext/shopping_cart/search.py b/erpnext/shopping_cart/search.py index 5d2de78f7c..30656befd6 100644 --- a/erpnext/shopping_cart/search.py +++ b/erpnext/shopping_cart/search.py @@ -111,7 +111,7 @@ class ProductSearch(FullTextSearch): ) def get_all_published_items(): - return frappe.get_all("Item", filters={"variant_of": "", "show_in_website": 1},pluck="name") + return frappe.get_all("Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code") def update_index_for_path(path): search = ProductSearch(INDEX_NAME) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index fb2ccef48f..b3edd9fc30 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -91,7 +91,7 @@ frappe.ui.form.on("Item", { erpnext.toggle_naming_series(); } - if(!frm.doc.published_in_website) { + if (!frm.doc.published_in_website) { frm.add_custom_button(__("Publish in Website"), function() { frappe.call({ method: "erpnext.e_commerce.doctype.website_item.website_item.make_website_item", @@ -102,8 +102,8 @@ frappe.ui.form.on("Item", { frappe.msgprint({ message: __("Website Item {0} has been created.", [repl('
%(item)s', { - item_encoded: encodeURIComponent(result.message[0]), - item: result.message[1] + item_encoded: encodeURIComponent(result.message[0]), + item: result.message[1] })] ), title: __("Published"), @@ -205,25 +205,8 @@ frappe.ui.form.on("Item", { } }, - copy_from_item_group: function(frm) { - return frm.call({ - doc: frm.doc, - method: "copy_specification_from_item_group" - }); - }, - has_variants: function(frm) { erpnext.item.toggle_attributes(frm); - }, - - show_in_website: function(frm) { - if (frm.doc.default_warehouse && !frm.doc.website_warehouse){ - frm.set_value("website_warehouse", frm.doc.default_warehouse); - } - }, - - set_meta_tags(frm) { - frappe.utils.set_meta_tag(frm.doc.route); } }); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 39cc9c75e2..82452b6543 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -116,24 +116,6 @@ "customer_code", "default_item_manufacturer", "default_manufacturer_part_no", - "website_section", - "published_in_website", - "show_in_website", - "show_variant_in_website", - "route", - "weightage", - "slideshow", - "website_image", - "website_image_alt", - "cb72", - "website_warehouse", - "website_item_groups", - "set_meta_tags", - "sb72", - "copy_from_item_group", - "website_specifications", - "web_long_description", - "website_content", "total_projected_qty", "hub_publishing_sb", "publish_in_hub", @@ -869,119 +851,6 @@ "no_copy": 1, "print_hide": 1 }, - { - "collapsible": 1, - "depends_on": "eval:!doc.is_fixed_asset", - "fieldname": "website_section", - "fieldtype": "Section Break", - "label": "Website", - "options": "fa fa-globe" - }, - { - "default": "0", - "depends_on": "eval:!doc.variant_of", - "fieldname": "show_in_website", - "fieldtype": "Check", - "label": "Show in Website", - "search_index": 1 - }, - { - "default": "0", - "depends_on": "variant_of", - "fieldname": "show_variant_in_website", - "fieldtype": "Check", - "label": "Show in Website (Variant)", - "search_index": 1 - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "route", - "fieldtype": "Small Text", - "label": "Route", - "no_copy": 1 - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "Items with higher weightage will be shown higher", - "fieldname": "weightage", - "fieldtype": "Int", - "label": "Weightage" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "Show a slideshow at the top of the page", - "fieldname": "slideshow", - "fieldtype": "Link", - "label": "Slideshow", - "options": "Website Slideshow" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "Item Image (if not slideshow)", - "fieldname": "website_image", - "fieldtype": "Attach", - "label": "Website Image" - }, - { - "fieldname": "cb72", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "Show \"In Stock\" or \"Not in Stock\" based on stock available in this warehouse.", - "fieldname": "website_warehouse", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Website Warehouse", - "options": "Warehouse" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "List this Item in multiple groups on the website.", - "fieldname": "website_item_groups", - "fieldtype": "Table", - "label": "Website Item Groups", - "options": "Website Item Group" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "set_meta_tags", - "fieldtype": "Button", - "label": "Set Meta Tags" - }, - { - "collapsible": 1, - "collapsible_depends_on": "website_specifications", - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "sb72", - "fieldtype": "Section Break", - "label": "Website Specifications" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "copy_from_item_group", - "fieldtype": "Button", - "label": "Copy From Item Group" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "website_specifications", - "fieldtype": "Table", - "label": "Website Specifications", - "options": "Item Website Specification" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "web_long_description", - "fieldtype": "Text Editor", - "label": "Website Description" - }, - { - "description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.", - "fieldname": "website_content", - "fieldtype": "HTML Editor", - "label": "Website Content" - }, { "fieldname": "total_projected_qty", "fieldtype": "Float", @@ -1057,24 +926,8 @@ "fieldtype": "Data", "label": "Default Manufacturer Part No", "read_only": 1 - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "website_image_alt", - "fieldtype": "Data", - "label": "Image Description" - }, - { - "default": "0", - "fieldname": "published_in_website", - "fieldtype": "Check", - "label": "Published in Website", - "no_copy": 1, - "read_only": 1, - "search_index": 1 } ], - "has_web_view": 1, "icon": "fa fa-tag", "idx": 2, "image_field": "image", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 2de4689f27..59bb7a0098 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -22,6 +22,7 @@ from frappe.utils import ( ) from frappe.utils.html_utils import clean_html from frappe.website.utils import clear_cache +from frappe.model.document import Document from frappe.website.website_generator import WebsiteGenerator import erpnext @@ -51,17 +52,8 @@ class InvalidBarcode(frappe.ValidationError): pass -class Item(WebsiteGenerator): - website = frappe._dict( - page_title_field="item_name", - condition_field="show_in_website", - template="templates/generators/item/item.html", - no_cache=1 - ) - +class Item(Document): def onload(self): - super(Item, self).onload() - self.set_onload('stock_exists', self.stock_ledger_created()) self.set_asset_naming_series() @@ -102,8 +94,6 @@ class Item(WebsiteGenerator): self.set_opening_stock() def validate(self): - super(Item, self).validate() - if not self.item_name: self.item_name = self.item_code @@ -143,9 +133,6 @@ class Item(WebsiteGenerator): if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") - self.old_website_item_groups = frappe.db.sql_list("""select item_group - from `tabWebsite Item Group` - where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name) def on_update(self): invalidate_cache_for_item(self) @@ -485,16 +472,6 @@ class Item(WebsiteGenerator): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) frappe.db.auto_commit_on_many_writes = 0 - @frappe.whitelist() - def copy_specification_from_item_group(self): - self.set("website_specifications", []) - if self.item_group: - for label, desc in frappe.db.get_values("Item Website Specification", - {"parent": self.item_group}, ["label", "description"]): - row = self.append("website_specifications") - row.label = label - row.description = desc - def update_bom_item_desc(self): if self.is_new(): return @@ -768,46 +745,6 @@ class Item(WebsiteGenerator): if not enabled: frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange") - def create_onboarding_docs(self, args): - company = frappe.defaults.get_defaults().get('company') or \ - frappe.db.get_single_value('Global Defaults', 'default_company') - - for i in range(1, args.get('max_count')): - item = args.get('item_' + str(i)) - if item: - default_warehouse = '' - default_warehouse = frappe.db.get_value('Warehouse', filters={ - 'warehouse_name': _('Finished Goods'), - 'company': company - }) - - try: - frappe.get_doc({ - 'doctype': self.doctype, - 'item_code': item, - 'item_name': item, - 'description': item, - 'is_sales_item': 1, - 'is_purchase_item': 1, - 'is_stock_item': 1, - 'item_group': _('Products'), - 'stock_uom': _(args.get('item_uom_' + str(i))), - 'item_defaults': [{ - 'default_warehouse': default_warehouse, - 'company': company - }] - }).insert() - - except frappe.NameError: - pass - else: - if args.get('item_price_' + str(i)): - item_price = flt(args.get('item_price_' + str(i))) - - price_list_name = frappe.db.get_value('Price List', {'selling': 1}) - make_item_price(item, price_list_name, item_price) - price_list_name = frappe.db.get_value('Price List', {'buying': 1}) - make_item_price(item, price_list_name, item_price) def make_item_price(item, price_list_name, item_price): frappe.get_doc({ @@ -924,12 +861,6 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): def invalidate_cache_for_item(doc): invalidate_cache_for(doc, doc.item_group) - website_item_groups = list(set((doc.get("old_website_item_groups") or []) - + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group])) - - for item_group in website_item_groups: - invalidate_cache_for(doc, item_group) - if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group: invalidate_cache_for(doc, doc.old_item_group) @@ -940,9 +871,10 @@ def invalidate_item_variants_cache_for_website(doc): from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager item_code = None - if doc.has_variants and doc.show_in_website: - item_code = doc.name - elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'show_in_website'): + is_web_item = doc.get("published_in_website") or doc.get("published") + if doc.has_variants and is_web_item: + item_code = doc.item_code + elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'published_in_website'): item_code = doc.variant_of if item_code: diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 6cec85288f..91c77d5152 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -40,9 +40,7 @@ "conversion_factor": 10.0 } ], - "stock_uom": "_Test UOM", - "show_in_website": 1, - "website_warehouse": "_Test Warehouse - _TC" + "stock_uom": "_Test UOM" }, { "description": "_Test Item 2", @@ -56,8 +54,6 @@ "item_group": "_Test Item Group", "item_name": "_Test Item 2", "stock_uom": "_Test UOM", - "show_in_website": 1, - "website_warehouse": "_Test Warehouse - _TC", "gst_hsn_code": "999800", "opening_stock": 10, "valuation_rate": 100, @@ -311,8 +307,7 @@ "warehouse_reorder_level": 20, "warehouse_reorder_qty": 20 } - ], - "show_in_website": 1 + ] }, { "description": "_Test Item 1", @@ -344,9 +339,7 @@ "warehouse_reorder_qty": 20 } ], - "stock_uom": "_Test UOM", - "show_in_website": 1, - "website_warehouse": "_Test Warehouse Group-C1 - _TC" + "stock_uom": "_Test UOM" }, { "description": "_Test Item With Item Tax Template", diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js index 488920aadb..5e1f7d5322 100644 --- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js +++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js @@ -7,9 +7,8 @@ frappe.ui.form.on('Item Variant Settings', { const existing_fields = frm.doc.fields.map(row => row.field_name); const exclude_fields = [...existing_fields, "naming_series", "item_code", "item_name", - "show_in_website", "show_variant_in_website", "standard_rate", "opening_stock", "image", - "variant_of", "valuation_rate", "barcodes", "website_image", "thumbnail", - "website_specifiations", "web_long_description", "has_variants", "attributes"]; + "published_in_website", "standard_rate", "opening_stock", "image", + "variant_of", "valuation_rate", "barcodes", "has_variants", "attributes"]; const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only']; diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py index cb6626fd17..bd9e9b9d55 100644 --- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py +++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals - import frappe from frappe import _ from frappe.model.document import Document @@ -15,7 +13,7 @@ class ItemVariantSettings(Document): def set_default_fields(self): self.fields = [] fields = frappe.get_meta('Item').fields - exclude_fields = {"naming_series", "item_code", "item_name", "show_in_website", + exclude_fields = {"naming_series", "item_code", "item_name", "published_in_website", "show_variant_in_website", "standard_rate", "opening_stock", "image", "description", "variant_of", "valuation_rate", "description", "barcodes", "website_image", "thumbnail", "website_specifiations", "web_long_description", diff --git a/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json b/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json deleted file mode 100644 index 5ee316786c..0000000000 --- a/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "add_more_button": 1, - "app": "ERPNext", - "creation": "2019-11-15 14:41:12.007359", - "docstatus": 0, - "doctype": "Onboarding Slide", - "domains": [], - "help_links": [], - "idx": 0, - "image_src": "", - "is_completed": 0, - "max_count": 3, - "modified": "2019-12-09 17:54:09.602885", - "modified_by": "Administrator", - "name": "Add A Few Products You Buy Or Sell", - "owner": "Administrator", - "ref_doctype": "Item", - "slide_desc": "", - "slide_fields": [ - { - "align": "", - "fieldname": "item", - "fieldtype": "Data", - "label": "Item", - "placeholder": "Product Name", - "reqd": 1 - }, - { - "align": "", - "fieldname": "item_price", - "fieldtype": "Currency", - "label": "Item Price", - "reqd": 1 - }, - { - "align": "", - "fieldtype": "Column Break", - "reqd": 0 - }, - { - "align": "", - "fieldname": "uom", - "fieldtype": "Link", - "label": "UOM", - "options": "UOM", - "reqd": 1 - } - ], - "slide_order": 30, - "slide_title": "Add A Few Products You Buy Or Sell", - "slide_type": "Create" -} \ No newline at end of file From b38339c979a18403af3824413045077d78e8ed96 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 19 Feb 2021 13:14:37 +0530 Subject: [PATCH 004/225] fix: Sider and indexing (minor) --- erpnext/e_commerce/doctype/website_item/website_item.py | 5 ++++- erpnext/portal/doctype/homepage/homepage.js | 8 ++++---- erpnext/stock/doctype/item/item.py | 5 ----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 51983ffd27..53ed5e8891 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -8,7 +8,6 @@ import json import itertools from frappe import _ -from frappe.website.render import clear_cache from frappe.website.website_generator import WebsiteGenerator from frappe.utils import cstr, random_string, cint, flt @@ -359,3 +358,7 @@ def make_website_item(doc): website_item.save() return [website_item.name, website_item.web_item_name] + +def on_doctype_update(): + # since route is a Text column, it needs a length for indexing + frappe.db.add_index("Website Item", ["route(500)"]) \ No newline at end of file diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js index ea34bb17e5..59f808a315 100644 --- a/erpnext/portal/doctype/homepage/homepage.js +++ b/erpnext/portal/doctype/homepage/homepage.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Homepage', { setup: function(frm) { - frm.fields_dict["products"].grid.get_field("item").get_query = function(){ + frm.fields_dict["products"].grid.get_field("item").get_query = function() { return { filters: {'published': 1} } @@ -21,9 +21,9 @@ frappe.ui.form.on('Homepage', { }); frappe.ui.form.on('Homepage Featured Product', { - view: function(frm, cdt, cdn){ - var child= locals[cdt][cdn] - if(child.item_code && child.route){ + view: function(frm, cdt, cdn) { + var child= locals[cdt][cdn]; + if (child.item_code && child.route) { window.open('/' + child.route, '_blank'); } } diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 59bb7a0098..ebc3276014 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -408,7 +408,6 @@ class Item(Document): if self.published_in_website: invalidate_cache_for_item(self) - clear_cache(self.route) frappe.db.set_value("Item", new_name, "item_code", new_name) @@ -998,10 +997,6 @@ def update_variants(variants, template, publish_progress=True): if publish_progress: frappe.publish_progress(count / total * 100, title=_("Updating Variants...")) -def on_doctype_update(): - # since route is a Text column, it needs a length for indexing - frappe.db.add_index("Item", ["route(500)"]) - @erpnext.allow_regional def set_item_tax_from_hsn_code(item): pass From f1ce418bdc0c3c522ffce58e79c92ad4a2dda079 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 19 Feb 2021 15:56:52 +0530 Subject: [PATCH 005/225] fix: Hide Attribute filters if 'Hide Variants' is enabled in E Commerce Settings - Hide Attribute filters if 'Hide Variants' is enabled in E Commerce Settings - Consider 'Hide Variants' in ProductQuery Engine - Added docstrings - Remove `get_e_commerce_settings`, redundant --- .../doctype/website_item/website_item.py | 1 + erpnext/portal/product_configurator/utils.py | 12 -- erpnext/shopping_cart/filters.py | 8 +- erpnext/shopping_cart/product_query.py | 6 +- erpnext/stock/doctype/item/item.py | 3 +- erpnext/www/all-products/index.html | 112 +++++++++--------- 6 files changed, 71 insertions(+), 71 deletions(-) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 53ed5e8891..64655716b4 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -326,6 +326,7 @@ class WebsiteItem(WebsiteGenerator): row.description = desc 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 invalidate_cache_for(doc, doc.item_group) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 0de97294f3..7ce8f805d4 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -138,15 +138,3 @@ def get_item_attributes(item_code): return attributes -def get_html_for_items(items): - html = [] - for item in items: - html.append(frappe.render_template('erpnext/www/all-products/item_row.html', { - 'item': item - })) - return html - -def get_e_commerce_settings(): - doc = frappe.get_cached_doc('E Commerce Settings') - doc.products_per_page = doc.products_per_page or 20 - return doc diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 4fe346b659..f074081ba3 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -1,8 +1,6 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - import frappe @@ -16,6 +14,8 @@ class ProductFiltersBuilder: self.item_group = item_group def get_field_filters(self): + if not self.doc.enable_field_filters: return + filter_fields = [row.fieldname for row in self.doc.filter_fields] meta = frappe.get_meta('Item') @@ -56,6 +56,8 @@ class ProductFiltersBuilder: return filter_data def get_attribute_filters(self): + if not self.doc.enable_attribute_filters: return + attributes = [row.attribute for row in self.doc.filter_attributes] if not attributes: diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 140e1c6b38..a2d2994bc1 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -42,6 +42,9 @@ class ProductQuery: 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 = [] @@ -53,7 +56,6 @@ class ProductQuery: filters=[["Website Item Group", "item_group", "=", item_group]] ) - self.query_fields = (", ").join(self.fields) if attributes: result = self.query_items_with_attributes(attributes, start) else: @@ -70,6 +72,8 @@ class ProductQuery: def query_items(self, conditions, or_conditions, substitutions, start=0): """Build a query to fetch Website Items based on field filters.""" + self.query_fields = (", ").join(self.fields) + return frappe.db.sql(""" select distinct {query_fields} from diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index ebc3276014..2185694d60 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -23,7 +23,6 @@ from frappe.utils import ( from frappe.utils.html_utils import clean_html from frappe.website.utils import clear_cache from frappe.model.document import Document -from frappe.website.website_generator import WebsiteGenerator import erpnext from erpnext.controllers.item_variant import ( @@ -858,6 +857,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): def invalidate_cache_for_item(doc): + """Invalidate Item Group cache and rebuild ItemVariantsCacheManager.""" invalidate_cache_for(doc, doc.item_group) if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group: @@ -867,6 +867,7 @@ def invalidate_cache_for_item(doc): def invalidate_item_variants_cache_for_website(doc): + """Rebuild ItemVariantsCacheManager via Item or Website Item.""" from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager item_code = None diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html index a7838eebc1..d32ef62275 100644 --- a/erpnext/www/all-products/index.html +++ b/erpnext/www/all-products/index.html @@ -53,68 +53,72 @@
{{ _('Filters') }}
{{ _('Clear All') }}
- {% for field_filter in field_filters %} - {%- set item_field = field_filter[0] %} - {%- set values = field_filter[1] %} -
-
{{ item_field.label }}
+ {% if field_filters %} + {% for field_filter in field_filters %} + {%- set item_field = field_filter[0] %} + {%- set values = field_filter[1] %} +
+
{{ item_field.label }}
- {% if values | len > 20 %} - - - {% endif %} + {% if values | len > 20 %} + + + {% endif %} - {% if values %} -
- {% for value in values %} -
- + {% if values %} +
+ {% for value in values %} +
+ +
+ {% endfor %}
- {% endfor %} + {% else %} + {{ _('No values') }} + {% endif %}
- {% else %} - {{ _('No values') }} - {% endif %} -
- {% endfor %} + {% endfor %} + {% endif %} - {% for attribute in attribute_filters %} -
-
{{ attribute.name}}
- {% if values | len > 20 %} - - - {% endif %} + {% if attribute_filters %} + {% for attribute in attribute_filters %} +
+
{{ attribute.name}}
+ {% if values | len > 20 %} + + + {% endif %} - {% if attribute.item_attribute_values %} -
- {% for attr_value in attribute.item_attribute_values %} -
- + {% if attribute.item_attribute_values %} +
+ {% for attr_value in attribute.item_attribute_values %} +
+ +
+ {% endfor %}
- {% endfor %} + {% else %} + {{ _('No values') }} + {% endif %}
- {% else %} - {{ _('No values') }} - {% endif %} -
- {% endfor %} + {% endfor %} + {% endif %}
{% endblock %} +{% block breadcrumbs %} +
+ {% include "templates/includes/breadcrumbs.html" %} +
+{% endblock %} + {% block page_content %}
@@ -27,6 +33,20 @@
+ {% if sub_categories %} +
+
{{ _('Sub Categories') }}
+
+
+ {% for row in sub_categories%} + +
+ {{ row.name }} +
+
+ {% endfor %} +
+ {% endif %}
{% if items %} {% for item in items %} diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/www/shop-by-category/category_card_section.html b/erpnext/www/shop-by-category/category_card_section.html new file mode 100644 index 0000000000..532a198661 --- /dev/null +++ b/erpnext/www/shop-by-category/category_card_section.html @@ -0,0 +1,28 @@ +{%- macro card(title, image, type, url=None, text_primary=False) -%} + +
+ {% if image %} + {{ title }} + {% else %} +
+ AB +
+ {% endif %} +
+ {{ title or '' }} +
+ +
+{%- endmacro -%} + +
+
+ {%- for row in data -%} + {%- set title = row.name -%} + {%- set image = row.get("image") -%} + {%- if title -%} + {{ card(title, image, type, row.get("route")) }} + {%- endif -%} + {%- endfor -%} +
+
\ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.html b/erpnext/www/shop-by-category/index.html new file mode 100644 index 0000000000..ac0b317665 --- /dev/null +++ b/erpnext/www/shop-by-category/index.html @@ -0,0 +1,60 @@ +{% extends "templates/web.html" %} +{% block title %}{{ _('Shop by Category') }}{% endblock %} + +{% block head_include %} + +{% endblock %} + +{% block script %} + +{% endblock %} + +{% block page_content %} +
+
+ {% if slideshow %} + + {{ web_block( + "Hero Slider", + values=slideshow, + add_container=0, + add_top_padding=0, + add_bottom_padding=0, + ) }} + {% endif %} +
+
+ {% if tabs %} + + {{ web_block( + "Section with Tabs", + values=tabs, + add_container=0, + add_top_padding=0, + add_bottom_padding=0 + ) }} + {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.js b/erpnext/www/shop-by-category/index.js new file mode 100644 index 0000000000..1b3116f5ba --- /dev/null +++ b/erpnext/www/shop-by-category/index.js @@ -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); + } + }); +}); \ No newline at end of file diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py new file mode 100644 index 0000000000..c295335cdf --- /dev/null +++ b/erpnext/www/shop-by-category/index.py @@ -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 + From 16b9c8c3838b8695216a968d05c296c95917925d Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 11 Mar 2021 10:56:00 +0530 Subject: [PATCH 009/225] feat: Card Actions and Wishlist - Rough UI for card actions - Wishlist doctype - Indicators on card based on stock availability --- .../e_commerce/doctype/wishlist/__init__.py | 0 .../doctype/wishlist/test_wishlist.py | 10 ++ .../e_commerce/doctype/wishlist/wishlist.js | 8 ++ .../e_commerce/doctype/wishlist/wishlist.json | 70 ++++++++++++ .../e_commerce/doctype/wishlist/wishlist.py | 10 ++ .../doctype/wishlist_items/__init__.py | 0 .../wishlist_items/wishlist_items.json | 104 ++++++++++++++++++ .../doctype/wishlist_items/wishlist_items.py | 10 ++ erpnext/e_commerce/product_query.py | 14 ++- erpnext/public/scss/shopping_cart.scss | 51 +++++++++ erpnext/templates/includes/macros.html | 52 ++++++--- erpnext/www/all-products/item_row.html | 2 +- 12 files changed, 315 insertions(+), 16 deletions(-) create mode 100644 erpnext/e_commerce/doctype/wishlist/__init__.py create mode 100644 erpnext/e_commerce/doctype/wishlist/test_wishlist.py create mode 100644 erpnext/e_commerce/doctype/wishlist/wishlist.js create mode 100644 erpnext/e_commerce/doctype/wishlist/wishlist.json create mode 100644 erpnext/e_commerce/doctype/wishlist/wishlist.py create mode 100644 erpnext/e_commerce/doctype/wishlist_items/__init__.py create mode 100644 erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json create mode 100644 erpnext/e_commerce/doctype/wishlist_items/wishlist_items.py diff --git a/erpnext/e_commerce/doctype/wishlist/__init__.py b/erpnext/e_commerce/doctype/wishlist/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/e_commerce/doctype/wishlist/test_wishlist.py b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py new file mode 100644 index 0000000000..6565e71b9e --- /dev/null +++ b/erpnext/e_commerce/doctype/wishlist/test_wishlist.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 TestWishlist(unittest.TestCase): + pass diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.js b/erpnext/e_commerce/doctype/wishlist/wishlist.js new file mode 100644 index 0000000000..d96e552ecd --- /dev/null +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.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('Wishlist', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.json b/erpnext/e_commerce/doctype/wishlist/wishlist.json new file mode 100644 index 0000000000..7e2c67478d --- /dev/null +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "autoname": "field:user", + "creation": "2021-03-10 18:52:28.769126", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "section_break_2", + "items" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Wishlist Items" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-03-10 19:05:52.373601", + "modified_by": "Administrator", + "module": "E-commerce", + "name": "Wishlist", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py new file mode 100644 index 0000000000..94e2754f88 --- /dev/null +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class Wishlist(Document): + pass diff --git a/erpnext/e_commerce/doctype/wishlist_items/__init__.py b/erpnext/e_commerce/doctype/wishlist_items/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json new file mode 100644 index 0000000000..29f40660a4 --- /dev/null +++ b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json @@ -0,0 +1,104 @@ +{ + "actions": [], + "creation": "2021-03-10 19:03:00.662714", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "website_item", + "column_break_3", + "item_name", + "item_details_section", + "description", + "column_break_7", + "image", + "image_view", + "warehouse_section", + "warehouse" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "website_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Website Item", + "options": "Website Item" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "collapsible": 1, + "fieldname": "item_details_section", + "fieldtype": "Section Break", + "label": "Item Details" + }, + { + "fetch_from": "item_code.description", + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fetch_from": "item_code.image", + "fieldname": "image", + "fieldtype": "Attach", + "hidden": 1, + "label": "Image" + }, + { + "fetch_from": "item_code.image", + "fieldname": "image_view", + "fieldtype": "Image", + "hidden": 1, + "label": "Image View", + "options": "image", + "print_hide": 1 + }, + { + "fieldname": "warehouse_section", + "fieldtype": "Section Break", + "label": "Warehouse" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse", + "options": "Warehouse" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-03-10 19:13:41.310816", + "modified_by": "Administrator", + "module": "E-commerce", + "name": "Wishlist Items", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.py b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.py new file mode 100644 index 0000000000..25ce17d655 --- /dev/null +++ b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class WishlistItems(Document): + pass diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py index f984ec2c6f..9743b768a5 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -22,7 +22,7 @@ class ProductQuery: self.page_length = self.settings.products_per_page or 20 self.fields = ['wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of', 'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description', - 'wi.route'] + 'wi.route', 'wi.website_warehouse'] self.conditions = "" self.or_conditions = "" self.substitutions = [] @@ -62,12 +62,22 @@ class ProductQuery: result = self.query_items(self.conditions, self.or_conditions, self.substitutions, start=start) - # add price info in results + # 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: item.formatted_price = (product_info.get('price') or {}).get('formatted_price') + 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" return result def query_items(self, conditions, or_conditions, substitutions, start=0): diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index da22aa6e4d..4317279186 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -519,3 +519,54 @@ body.product-page { border: 1px solid var(--dark-border-color); } } + +.card-indicator { + margin-left: 6px; +} + +.like-action { + text-align: center; + margin-top: -2px; + margin-left: 12px; +} + +.wish-icon { + cursor: pointer; + stroke: #F47A7A !important; + + &:hover { + fill: #F47A7A; + } +} + +.wished { + .wish-icon { + stroke: none; + fill: #F47A7A !important; + } +} + +.list-row-checkbox { + &:before { + display: none; + } + + &:checked:before { + display: block; + z-index: 1; + } +} + +.btn-add-to-cart-list { + color: var(--blue-500); + background-color: white; + box-shadow: none; + border: 1px solid var(--blue-500); + margin: var(--margin-sm) 0; + flex: none; + + &:hover { + background-color: var(--blue-500); + color: white; + } +} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index be0d47f371..73c874542a 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -59,7 +59,7 @@ {% endmacro %} -{%- macro item_card(title, image, url, description, rate, category, is_featured=False, is_full_width=False, align="Left") -%} +{%- macro item_card(title, image, url, description, rate, category, in_stock=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', @@ -89,35 +89,61 @@
{% if image %} -
- {{ title }} -
+
+ + {{ title }} + +
{% else %} -
- {{ frappe.utils.get_abbr(title) }} -
+ +
+ {{ frappe.utils.get_abbr(title) }} +
+
{% endif %} - {{ item_card_body(title, description, url, rate, category, is_featured, align) }} + {{ item_card_body(title, description, url, rate, category, is_featured, align, in_stock) }}
{% endif %} {%- endmacro -%} -{%- macro item_card_body(title, description, url, rate, category, is_featured, align) -%} +{%- macro item_card_body(title, description, url, rate, category, is_featured, align, in_stock=None) -%} {%- set align_class = resolve_class({ 'text-right': align == 'Right', 'text-center': align == 'Center' and not is_featured, 'text-left': align == 'Left' or is_featured, }) -%} -
-
{{ title or '' }}
+
+ +
+ +
{{ title or '' }}
+
+ {% if in_stock %} + + {% endif %} + + +
{% if is_featured %}
{{ rate or '' }}
{{ description or '' }}
{% else %}
{{ category or '' }}
-
{{ rate or '' }}
+
+ {% if rate %} +
{{ rate or '' }}
+ {% endif %} +
+ {{ _('Add to Cart') }} +
+
{% endif %}
- {%- endmacro -%} diff --git a/erpnext/www/all-products/item_row.html b/erpnext/www/all-products/item_row.html index a7e994c1e3..665936df19 100644 --- a/erpnext/www/all-products/item_row.html +++ b/erpnext/www/all-products/item_row.html @@ -2,5 +2,5 @@ {{ item_card( item.item_name or item.name, item.website_image or item.image, item.route, item.website_description or item.description, - item.formatted_price, item.item_group + item.formatted_price, item.item_group, in_stock=item.in_stock ) }} From 4f64d1c7f23915570fb523aca37cd5bfce3ecb37 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 11 Mar 2021 21:24:47 +0530 Subject: [PATCH 010/225] feat: Animate Add to Cart List interactions (UX) - Increased qty in cart on clicking add to cart for existing item - Simplified macro arguments - Navbar cart icon animation - Explore button for template item in card - Add to cart button animation --- erpnext/e_commerce/shopping_cart/cart.py | 8 +-- .../item_card_group/item_card_group.html | 3 +- erpnext/public/js/shopping_cart.js | 7 +- erpnext/public/scss/shopping_cart.scss | 67 +++++++++++++++++-- erpnext/templates/includes/macros.html | 65 +++++++++++------- erpnext/www/all-products/index.js | 30 ++++++++- erpnext/www/all-products/item_row.html | 6 +- 7 files changed, 142 insertions(+), 44 deletions(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index a5f38c69ea..c2c94a3664 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -138,7 +138,7 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): "additional_notes": additional_notes }) else: - quotation_items[0].qty = qty + quotation_items[0].qty = qty + 1 quotation_items[0].additional_notes = additional_notes apply_cart_settings(quotation=quotation) @@ -153,9 +153,8 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): set_cart_count(quotation) - context = get_cart_quotation(quotation) - if cint(with_items): + context = get_cart_quotation(quotation) return { "items": frappe.render_template("templates/includes/cart/cart_items.html", context), @@ -164,8 +163,7 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): } else: return { - 'name': quotation.name, - 'shopping_cart_menu': get_shopping_cart_menu(context) + 'name': quotation.name } @frappe.whitelist() 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 fe061d5f5f..889a228168 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 @@ -25,8 +25,7 @@ {%- if item -%} {%- set item = frappe.get_doc("Item", item) -%} {{ item_card( - item.item_name, item.image, item.route, item.description, - None, item.item_group, values['card_' + index + '_featured'], + item, is_featured=values['card_' + index + '_featured'], True, "Center" ) }} {%- endif -%} diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index 6553801de0..bcfa983e4a 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -93,9 +93,6 @@ $.extend(shopping_cart, { btn: opts.btn, callback: function(r) { shopping_cart.set_cart_count(); - if (r.message.shopping_cart_menu) { - $('.shopping-cart-menu').html(r.message.shopping_cart_menu); - } if(opts.callback) opts.callback(r); } @@ -129,6 +126,10 @@ $.extend(shopping_cart, { if(cart_count) { $badge.html(cart_count); + $cart.addClass('cart-animate'); + setTimeout(() => { + $cart.removeClass('cart-animate'); + }, 500); } else { $badge.remove(); } diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 4317279186..8380f6cf33 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -363,6 +363,31 @@ body.product-page { } } +.cart-animate { + animation: wiggle 0.5s linear; +} +@keyframes wiggle { + 8%, + 41% { + transform: translateX(-10px); + } + 25%, + 58% { + transform: translate(10px); + } + 75% { + transform: translate(-5px); + } + 92% { + transform: translate(5px); + } + 0%, + 100% { + transform: translate(0); + } +} + + #page-cart { .shopping-cart-header { @@ -557,16 +582,50 @@ body.product-page { } } -.btn-add-to-cart-list { +.btn-explore-variants { + box-shadow: none; + margin: var(--margin-sm) 0; + margin-left: 18px; + max-height: 30px; // to avoid resizing on window resize + flex: none; + transition: 0.3s ease; + color: var(--orange-500); + background-color: white; + border: 1px solid var(--orange-500); + + &:hover { + color: white; + background-color: var(--orange-500); + } +} + +.btn-add-to-cart-list{ + box-shadow: none; + margin: var(--margin-sm) 0; + max-height: 30px; // to avoid resizing on window resize + flex: none; + transition: 0.3s ease; +} + +.not-added { + margin-left: 18px; color: var(--blue-500); background-color: white; - box-shadow: none; border: 1px solid var(--blue-500); - margin: var(--margin-sm) 0; - flex: none; &:hover { background-color: var(--blue-500); color: white; } } + +.added-to-cart { + margin-left: 18px; + background-color: var(--dark-green-400); + color: white; + border: 2px solid var(--green-300); + + &:hover { + color: white; + } +} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 73c874542a..818316c0cf 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -59,13 +59,17 @@ {% endmacro %} -{%- macro item_card(title, image, url, description, rate, category, in_stock=None, is_featured=False, is_full_width=False, align="Left") -%} +{%- macro item_card(item, 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', 'align-items-start': align == 'Left', }) -%} {%- set col_size = 3 if is_full_width else 4 -%} +{%- set title = item.item_name or item.item_code -%} +{%- set image = item.website_image or item.image -%} +{%- set description = item.website_description or item.description-%} + {% if is_featured %}
- {{ item_card_body(title, description, url, rate, category, is_featured, align) }} + {{ item_card_body(title, description, item, is_featured, align) }}
{% else %}
- {{ item_card_body(title, description, url, rate, category, is_featured, align) }} + {{ item_card_body(title, description, item, is_featured, align) }}
{% endif %}
@@ -90,24 +94,24 @@
{% if image %} {% else %} - +
{{ frappe.utils.get_abbr(title) }}
{% endif %} - {{ item_card_body(title, description, url, rate, category, is_featured, align, in_stock) }} + {{ item_card_body(title, description, item, is_featured, align) }}
{% endif %} {%- endmacro -%} -{%- macro item_card_body(title, description, url, rate, category, is_featured, align, in_stock=None) -%} +{%- macro item_card_body(title, description, item, is_featured, align) -%} {%- set align_class = resolve_class({ 'text-right': align == 'Right', 'text-center': align == 'Center' and not is_featured, @@ -116,33 +120,44 @@
- +
{{ title or '' }}
- {% if in_stock %} - + {% if item.in_stock %} + + {% endif %} + {% if not item.has_variants %} + + {% endif %} - -
{% if is_featured %} -
{{ rate or '' }}
+
{{ item.formatted_price or '' }}
{{ description or '' }}
{% else %} -
{{ category or '' }}
+
{{ item.item_group or '' }}
- {% if rate %} -
{{ rate or '' }}
+ {% if item.formatted_price %} +
{{ item.formatted_price or '' }}
+ {% endif %} + {% if item.has_variants %} + +
+ {{ _('Explore') }} +
+
+ {% else %} +
+ {{ _('Add to Cart') }} +
{% endif %} -
- {{ _('Add to Cart') }} -
{% endif %}
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 37e07f4c7e..4572ee7370 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -2,6 +2,7 @@ $(() => { class ProductListing { constructor() { this.bind_filters(); + this.bind_card_actions(); this.bind_search(); this.restore_filters_state(); } @@ -71,8 +72,35 @@ $(() => { }, 1000)); } - make_filters() { + bind_card_actions() { + $('.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; + button.removeClass('not-added'); + button.addClass('added-to-cart'); + $(btn_id).text('Added to Cart'); + + // undo + setTimeout(() => { + button.removeClass('added-to-cart'); + button.addClass('not-added'); + $(btn_id).text('Add to Cart'); + }, 2000); } bind_search() { diff --git a/erpnext/www/all-products/item_row.html b/erpnext/www/all-products/item_row.html index 665936df19..072e35946b 100644 --- a/erpnext/www/all-products/item_row.html +++ b/erpnext/www/all-products/item_row.html @@ -1,6 +1,4 @@ {% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %} -{{ item_card( - item.item_name or item.name, item.website_image or item.image, item.route, item.website_description or item.description, - item.formatted_price, item.item_group, in_stock=item.in_stock -) }} +{{ item_card(item) }} + From 96cc5068b29ae5a20d7f9dd3608a6ce05db04a09 Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 14 Mar 2021 17:28:49 +0530 Subject: [PATCH 011/225] feat: Wishlist from card actions - Add remove items from wishlist - Wishlist icon at nav bar - Animate wishlist icon in card and navbar - Remember wished state after refresh as well --- .../e_commerce/doctype/wishlist/wishlist.py | 46 +++++++++++- .../wishlist_items/wishlist_items.json | 16 ++++- erpnext/e_commerce/product_query.py | 6 ++ erpnext/e_commerce/shopping_cart/cart.py | 2 +- erpnext/public/build.json | 3 +- erpnext/public/js/wishlist.js | 39 ++++++++++ erpnext/public/scss/shopping_cart.scss | 53 +++++++++----- erpnext/templates/includes/macros.html | 7 +- .../includes/navbar/navbar_items.html | 10 ++- erpnext/www/all-products/index.js | 72 +++++++++++++++++-- 10 files changed, 224 insertions(+), 30 deletions(-) create mode 100644 erpnext/public/js/wishlist.js diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py index 94e2754f88..7527c6f415 100644 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.py +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py @@ -3,8 +3,52 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document class Wishlist(Document): pass + +@frappe.whitelist() +def add_to_wishlist(item_code, price): + """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) + + wished_item_dict = { + "item_code": item_code, + "item_name": web_item_data.get("item_name"), + "website_item": web_item_data.get("name"), + "price": frappe.utils.flt(price), + "image": web_item_data.get("image"), + "website_warehouse": web_item_data.get("website_warehouse") + } + + if not frappe.db.exists("Wishlist", frappe.session.user): + # initialise wishlist + wishlist = frappe.get_doc({"doctype": "Wishlist"}) + wishlist.user = frappe.session.user + wishlist.append("items", wished_item_dict) + wishlist.save(ignore_permissions=True) + else: + wishlist = frappe.get_doc("Wishlist", frappe.session.user) + item = wishlist.append('items', wished_item_dict) + item.db_insert() + + if hasattr(frappe.local, "cookie_manager"): + frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items))) + +@frappe.whitelist() +def remove_from_wishlist(item_code): + if frappe.db.exists("Wishlist Items", {"item_code": item_code}): + frappe.db.sql(""" + delete + from `tabWishlist Items` + where item_code=%(item_code)s + """%{"item_code": frappe.db.escape(item_code)}) + + frappe.db.commit() + + wishlist = frappe.get_doc("Wishlist", frappe.session.user) + if hasattr(frappe.local, "cookie_manager"): + frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items))) \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json index 29f40660a4..18065a8861 100644 --- a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json +++ b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json @@ -12,6 +12,8 @@ "item_details_section", "description", "column_break_7", + "section_break_8", + "price", "image", "image_view", "warehouse_section", @@ -52,6 +54,7 @@ }, { "fetch_from": "item_code.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Text Editor", "label": "Description" @@ -62,6 +65,7 @@ }, { "fetch_from": "item_code.image", + "fetch_if_empty": 1, "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -69,6 +73,7 @@ }, { "fetch_from": "item_code.image", + "fetch_if_empty": 1, "fieldname": "image_view", "fieldtype": "Image", "hidden": 1, @@ -87,12 +92,21 @@ "in_list_view": 1, "label": "Warehouse", "options": "Warehouse" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "price", + "fieldtype": "Float", + "label": "Price" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-03-10 19:13:41.310816", + "modified": "2021-03-12 18:23:03.487891", "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 9743b768a5..28d33e6e81 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -67,6 +67,7 @@ class ProductQuery: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: 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( @@ -78,6 +79,11 @@ class ProductQuery: "actual_qty") ) item.in_stock = "green" if stock_qty else "red" + + item.wished = False + if frappe.db.exists("Wishlist Items", {"item_code": item.item_code}): + item.wished = True + return result def query_items(self, conditions, or_conditions, substitutions, start=0): diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index c2c94a3664..011f29cdc6 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -138,7 +138,7 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): "additional_notes": additional_notes }) else: - quotation_items[0].qty = qty + 1 + quotation_items[0].qty = qty quotation_items[0].additional_notes = additional_notes apply_cart_settings(quotation=quotation) diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 6b70dab803..a8911216f7 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -11,7 +11,8 @@ ], "js/erpnext-web.min.js": [ "public/js/website_utils.js", - "public/js/shopping_cart.js" + "public/js/shopping_cart.js", + "public/js/wishlist.js" ], "css/erpnext-web.css": [ "public/scss/website.scss", diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js new file mode 100644 index 0000000000..328bdb996d --- /dev/null +++ b/erpnext/public/js/wishlist.js @@ -0,0 +1,39 @@ +frappe.provide("erpnext.e_commerce"); +var wishlist = erpnext.e_commerce; + +frappe.ready(function() { + $(".wishlist").toggleClass('hidden', true); + wishlist.set_wishlist_count(); +}); + +$.extend(wishlist, { + set_wishlist_count: function() { + var wish_count = frappe.get_cookie("wish_count"); + if(frappe.session.user==="Guest") { + wish_count = 0; + } + + if(wish_count) { + $(".wishlist").toggleClass('hidden', false); + } + + var $wishlist = $('.wishlist-icon'); + var $badge = $wishlist.find("#wish-count"); + + if(parseInt(wish_count) === 0 || wish_count === undefined) { + $wishlist.css("display", "none"); + } + else { + $wishlist.css("display", "inline"); + } + if(wish_count) { + $badge.html(wish_count); + $wishlist.addClass('cart-animate'); + setTimeout(() => { + $wishlist.removeClass('cart-animate'); + }, 500); + } else { + $badge.remove(); + } + } +}); \ No newline at end of file diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 8380f6cf33..3d66f146c0 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -349,20 +349,20 @@ body.product-page { } } -.cart-icon { - .cart-badge { - position: relative; - top: -10px; - left: -12px; - background: var(--red-600); - width: 16px; - align-items: center; - height: 16px; - font-size: 10px; - border-radius: 50%; - } + +.shopping-badge { + position: relative; + top: -10px; + left: -12px; + background: var(--red-600); + width: 16px; + align-items: center; + height: 16px; + font-size: 10px; + border-radius: 50%; } + .cart-animate { animation: wiggle 0.5s linear; } @@ -555,7 +555,28 @@ body.product-page { margin-left: 12px; } -.wish-icon { +.like-animate { + animation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1; +} + +@keyframes expand { + 30% { + transform: scale(1.6); + } + 50% { + transform: scale(0.8); + } + 70% { + transform: scale(1.3); + } + 100% { + transform: scale(1); + } + } + +@keyframes heart { 0%, 17.5% { font-size: 0; } } + +.not-wished { cursor: pointer; stroke: #F47A7A !important; @@ -565,10 +586,8 @@ body.product-page { } .wished { - .wish-icon { - stroke: none; - fill: #F47A7A !important; - } + stroke: none; + fill: #F47A7A !important; } .list-row-checkbox { diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 818316c0cf..743daaf7e1 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -127,12 +127,11 @@ {% endif %} {% if not item.has_variants %} - {% endif %} diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html index 291220629c..54ed98af88 100644 --- a/erpnext/templates/includes/navbar/navbar_items.html +++ b/erpnext/templates/includes/navbar/navbar_items.html @@ -6,7 +6,15 @@ - + + + + {% endblock %} diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 4572ee7370..fc1a3f4dc3 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -73,6 +73,11 @@ $(() => { } 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); @@ -91,18 +96,77 @@ $(() => { animate_add_to_cart(button) { // Create 'added to cart' animation let btn_id = "#" + button[0].id; - button.removeClass('not-added'); - button.addClass('added-to-cart'); + this.toggle_button_class(button, 'not-added', 'added-to-cart'); $(btn_id).text('Added to Cart'); // undo setTimeout(() => { - button.removeClass('added-to-cart'); - button.addClass('not-added'); + 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); + } + bind_search() { $('input[type=search]').on('keydown', (e) => { if (e.keyCode === 13) { From 59514408b9600d0523b657cdb9968b42724df1bb Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 16 Mar 2021 00:05:53 +0530 Subject: [PATCH 012/225] 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 --- .../e_commerce/doctype/wishlist/wishlist.py | 10 +- .../wishlist_items/wishlist_items.json | 25 +++- erpnext/e_commerce/product_query.py | 2 +- erpnext/public/js/shopping_cart.js | 35 +++++ erpnext/public/js/wishlist.js | 133 +++++++++++++++++- erpnext/public/scss/shopping_cart.scss | 24 ++++ erpnext/templates/includes/macros.html | 65 ++++++++- .../includes/navbar/navbar_items.html | 2 +- erpnext/templates/pages/wishlist.html | 24 ++++ erpnext/templates/pages/wishlist.py | 39 +++++ erpnext/www/all-products/index.js | 94 +------------ 11 files changed, 345 insertions(+), 108 deletions(-) create mode 100644 erpnext/templates/pages/wishlist.html create mode 100644 erpnext/templates/pages/wishlist.py diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py index 7527c6f415..83bdff6115 100644 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.py +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py @@ -10,18 +10,22 @@ class Wishlist(Document): 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 18065a8861..0b132737b3 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 28d33e6e81..c37f8fb6b2 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -81,7 +81,7 @@ class ProductQuery: 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 bcfa983e4a..b57862b93a 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -186,5 +186,40 @@ $.extend(shopping_cart, { $(".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 328bdb996d..6ab1906b60 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 @@ $.extend(wishlist, { } 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 3d66f146c0..6a96e41256 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -648,3 +648,27 @@ body.product-page { 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 743daaf7e1..aec201e0d9 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -118,7 +118,6 @@ 'text-left': align == 'Left' or is_featured, }) -%}
-
{{ title or '' }}
@@ -128,7 +127,9 @@ {% endif %} {% if not item.has_variants %} {%- endmacro -%} + + +{%- macro wishlist_card(item, settings) %} +
+
+ {% if item.image %} +
+ + {{ title }} + +
+ + + + + +
+ +
+ {% else %} + +
+ {{ frappe.utils.get_abbr(title) }} +
+
+ {% endif %} + + {{ wishlist_card_body(item, settings) }} + + +
+
+{%- endmacro -%} + +{%- macro wishlist_card_body(item, settings) %} +
+
+
{{ item.item_name or item.item_code or ''}}
+
+
{{ item.formatted_price or '' }}
+ + {% if (item.available and settings.show_stock_availability) or (not settings.show_stock_availability) %} + + + {% else %} +
+ {{ _("Not in Stock") }} +
+ {% endif %} +
+{%- endmacro -%} diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html index 54ed98af88..793bacb665 100644 --- a/erpnext/templates/includes/navbar/navbar_items.html +++ b/erpnext/templates/includes/navbar/navbar_items.html @@ -10,7 +10,7 @@ - + {% 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) }} From c842305be01cbe6105e8f9b78c4d837db675d8a2 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 7 Apr 2021 23:49:04 +0530 Subject: [PATCH 017/225] feat: Customer Ratings & Reviews Full Page - Created macros for repetitive snippets - Created Customer Reviews full page - View more button to reveal 10 more reviews at a time - Common function to get reviews with start and end --- .../doctype/item_review/item_review.json | 18 ++- .../doctype/item_review/item_review.py | 36 ++++- .../doctype/website_item/website_item.py | 25 +--- erpnext/e_commerce/product_query.py | 2 +- erpnext/public/scss/shopping_cart.scss | 1 - .../setup/doctype/item_group/item_group.py | 2 +- .../generators/item/item_reviews.html | 63 ++------ erpnext/templates/includes/macros.html | 56 ++++++++ erpnext/templates/pages/customer_reviews.html | 45 ++++++ erpnext/templates/pages/customer_reviews.js | 135 ++++++++++++++++++ erpnext/templates/pages/customer_reviews.py | 16 +++ erpnext/templates/pages/reviews.html | 0 12 files changed, 316 insertions(+), 83 deletions(-) create mode 100644 erpnext/templates/pages/customer_reviews.html create mode 100644 erpnext/templates/pages/customer_reviews.js create mode 100644 erpnext/templates/pages/customer_reviews.py delete mode 100644 erpnext/templates/pages/reviews.html diff --git a/erpnext/e_commerce/doctype/item_review/item_review.json b/erpnext/e_commerce/doctype/item_review/item_review.json index 918f433935..7b6071b41f 100644 --- a/erpnext/e_commerce/doctype/item_review/item_review.json +++ b/erpnext/e_commerce/doctype/item_review/item_review.json @@ -79,13 +79,14 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-03-24 22:27:28.094535", + "modified": "2021-04-02 15:56:00.447950", "modified_by": "Administrator", "module": "E-commerce", "name": "Item Review", "owner": "Administrator", "permissions": [ { + "create": 1, "delete": 1, "email": 1, "export": 1, @@ -93,9 +94,11 @@ "read": 1, "report": 1, "role": "System Manager", - "share": 1 + "share": 1, + "write": 1 }, { + "create": 1, "delete": 1, "email": 1, "export": 1, @@ -103,6 +106,17 @@ "read": 1, "report": 1, "role": "Website Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "Customer", "share": 1 } ], diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py index bbb85b3d5d..84a1274d4c 100644 --- a/erpnext/e_commerce/doctype/item_review/item_review.py +++ b/erpnext/e_commerce/doctype/item_review/item_review.py @@ -4,14 +4,48 @@ from __future__ import unicode_literals from datetime import datetime +from six import string_types +import json + import frappe from frappe.model.document import Document - from frappe.contacts.doctype.contact.contact import get_contact_name +from frappe.utils import flt, cint +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings class ItemReview(Document): pass +@frappe.whitelist() +def get_item_reviews(web_item, start, end, data=None): + if not data: + data = frappe._dict() + + settings = get_shopping_cart_settings() + + if settings and settings.get("enable_reviews"): + data.reviews = frappe.db.get_all("Item Review", filters={"website_item": web_item}, + fields=["*"], limit_start=cint(start), limit_page_length=cint(end)) + + rating_data = frappe.db.get_all("Item Review", filters={"website_item": web_item}, + fields=["avg(rating) as average, count(*) as total"])[0] + data.average_rating = flt(rating_data.average, 1) + data.average_whole_rating = flt(data.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={"website_item": web_item, "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) + + data.reviews_per_rating = reviews_per_rating + data.total_reviews = rating_data.total + + return data + @frappe.whitelist() def add_item_review(web_item, title, rating, comment=None): """ Add an Item Review by a user if non-existent. """ diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index d8ebffcca2..a7c56cf681 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -14,6 +14,7 @@ from frappe.utils import cstr, random_string, cint, flt from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) +from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews class WebsiteItem(WebsiteGenerator): website = frappe._dict( @@ -176,7 +177,7 @@ class WebsiteItem(WebsiteGenerator): self.set_metatags(context) self.set_shopping_cart_data(context) self.get_product_details_section(context) - self.get_reviews(context) + get_item_reviews(self.name, 0, 4, context) context.wished = False if frappe.db.exists("Wishlist Items", {"item_code": self.item_code, "parent": frappe.session.user}): @@ -362,28 +363,6 @@ 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/product_query.py b/erpnext/e_commerce/product_query.py index 6ffab56229..37e91a09e9 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -67,7 +67,7 @@ class ProductQuery: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: item.formatted_price = (product_info.get('price') or {}).get('formatted_price') - item.price = product_info['price'].get('price_list_rate') + item.price = (product_info.get('price') or {}).get('price_list_rate') if self.settings.show_stock_availability: if item.get("website_warehouse"): diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 8d7f59de27..905d7e6d49 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -705,7 +705,6 @@ body.product-page { .ratings-reviews-section { border-top: 1px solid #E2E6E9; - display: flex; } .reviews-header { diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index a9e36799ad..cdff775f74 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -170,7 +170,7 @@ def get_item_for_list_in_html(context): 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: + if from_item and frappe.request.environ.get("HTTP_REFERER"): # 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"): diff --git a/erpnext/templates/generators/item/item_reviews.html b/erpnext/templates/generators/item/item_reviews.html index c271fdb808..f6b1831cda 100644 --- a/erpnext/templates/generators/item/item_reviews.html +++ b/erpnext/templates/generators/item/item_reviews.html @@ -1,47 +1,16 @@ -{% from "erpnext/templates/includes/macros.html" import ratings_with_title %} +{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %} -
- +
-

- {{ _("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 %} -
+ {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }} {% if frappe.session.user != "Guest" %} - {% endif %} -
@@ -50,28 +19,14 @@ {{ _("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 %} + {{ user_review(reviews) }} {% if total_reviews > 4 %} {% endif %} + {% else %}
{{ _("No Reviews") }} @@ -96,7 +51,6 @@ ], primary_action: function() { var data = d.get_values(); - $btn.prop('hidden', true); frappe.call({ method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review", args: { @@ -115,6 +69,7 @@ indicator: "green" }); d.hide(); + location.reload(); } } }); diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index e05bc636fd..cd29494e03 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -236,3 +236,59 @@

{%- endmacro -%} + +{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating)-%} + +

+ {{ _("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 %} +
+{%- endmacro -%} + +{%- macro user_review(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 %} +
+{%- endmacro -%} diff --git a/erpnext/templates/pages/customer_reviews.html b/erpnext/templates/pages/customer_reviews.html new file mode 100644 index 0000000000..9d8ba9e304 --- /dev/null +++ b/erpnext/templates/pages/customer_reviews.html @@ -0,0 +1,45 @@ +{% extends "templates/web.html" %} +{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %} + +{% block title %} {{ _("Customer Reviews") }} {% endblock %} + +{% block page_content %} +
+
+
+ {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }} + + + {% if frappe.session.user != "Guest" %} + + {% endif %} +
+ + +
+

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

+ {% if reviews %} + {{ user_review(reviews) }} + + {% if not reviews | len >= total_reviews %} + + {% endif %} + + {% else %} +
+ {{ _("No Reviews") }} +
+ {% endif %} +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/erpnext/templates/pages/customer_reviews.js b/erpnext/templates/pages/customer_reviews.js new file mode 100644 index 0000000000..453b96a168 --- /dev/null +++ b/erpnext/templates/pages/customer_reviews.js @@ -0,0 +1,135 @@ +$(() => { + class CustomerReviews { + constructor() { + this.bind_button_actions(); + this.start = 0; + this.page_length = 10; + } + + bind_button_actions() { + this.write_review(); + this.view_more(); + } + + write_review() { + //TODO: make dialog popup on stray page + $('.page_content').on('click', '.btn-write-review', (e) => { + // Bind action on write a review button + const $btn = $(e.currentTarget); + + let d = new frappe.ui.Dialog({ + title: __("Write a Review"), + fields: [ + {fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1}, + {fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1}, + {fieldtype: "Section Break"}, + {fieldname: "comment", fieldtype: "Small Text", label: "Your Review"} + ], + primary_action: function() { + let me = this; + let data = d.get_values(); + frappe.call({ + method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review", + args: { + web_item: $btn.attr('data-web-item'), + title: data.title, + rating: data.rating, + comment: data.comment + }, + freeze: true, + freeze_message: __("Submitting Review ..."), + callback: (r) => { + if(!r.exc) { + frappe.msgprint({ + message: __("Thank you for submitting your review"), + title: __("Review Submitted"), + indicator: "green" + }); + d.hide(); + location.reload(); + } + } + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }); + } + + view_more() { + $('.page_content').on('click', '.btn-view-more', (e) => { + // Bind action on view more button + const $btn = $(e.currentTarget); + $btn.prop('disabled', true); + + this.start += this.page_length; + let me = this; + + frappe.call({ + method: "erpnext.e_commerce.doctype.item_review.item_review.get_item_reviews", + args: { + web_item: $btn.attr('data-web-item'), + start: me.start, + end: me.page_length + }, + callback: (result) => { + if(result.message) { + let res = result.message; + me.get_user_review_html(res.reviews); + + $btn.prop('disabled', false); + if (res.total_reviews <= (me.start + me.page_length)) { + $btn.hide(); + } + + } + } + }) + }); + + } + + get_user_review_html(reviews) { + let me = this; + let $content = $('.user-reviews'); + + reviews.forEach((review) => { + $content.append(` +
+
+
+ ${me.get_review_stars(review.rating)} +
+

+ ${__(review.review_title)} +

+
+
+ ${__(review.customer)} + ${__(review.published_on)} +
+
+

+ ${__(review.comment)} +

+
+
+ `); + }); + } + + get_review_stars(rating) { + let stars = ``; + for(let i = 1; i < 6; i++) { + let fill_class = i <= rating ? 'star-click' : ''; + stars += ` + + `; + } + return stars; + } + } + + new CustomerReviews(); +}); \ No newline at end of file diff --git a/erpnext/templates/pages/customer_reviews.py b/erpnext/templates/pages/customer_reviews.py new file mode 100644 index 0000000000..3bb0142d0a --- /dev/null +++ b/erpnext/templates/pages/customer_reviews.py @@ -0,0 +1,16 @@ +# 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.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") + get_item_reviews(context.web_item, 0, 10, context) diff --git a/erpnext/templates/pages/reviews.html b/erpnext/templates/pages/reviews.html deleted file mode 100644 index e69de29bb2..0000000000 From 3d5f029e51e9ceae62b9c141291f1725a286c5d7 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 8 Apr 2021 15:25:43 +0530 Subject: [PATCH 018/225] fix: Sider and Tests --- .../e_commerce_settings.py | 9 ++-- .../doctype/item_review/item_review.py | 3 -- .../doctype/website_item/website_item.py | 18 +++---- .../e_commerce/doctype/wishlist/wishlist.py | 2 +- erpnext/e_commerce/filters.py | 9 ++-- .../test_product_configurator.py | 2 +- .../e_commerce/product_configurator/utils.py | 3 +- erpnext/e_commerce/product_query.py | 47 ++++++++++--------- .../make_homepage_products_website_items.py | 3 +- erpnext/stock/doctype/item/item.py | 1 - erpnext/templates/pages/customer_reviews.js | 9 ++-- erpnext/templates/pages/wishlist.py | 4 +- 12 files changed, 59 insertions(+), 51 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index a6e21b5f47..2de47aa4ea 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -3,7 +3,8 @@ # For license information, please see license.txt import frappe -from frappe import _ +from frappe.utils import cint +from frappe import _, msgprint from frappe.model.document import Document from frappe.utils import cint from frappe.utils import get_datetime, get_datetime_str, now_datetime @@ -29,7 +30,8 @@ class ECommerceSettings(Document): frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings") def validate_field_filters(self): - if not (self.enable_field_filters and self.filter_fields): return + if not (self.enable_field_filters and self.filter_fields): + return item_meta = frappe.get_meta("Item") valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]] @@ -39,7 +41,8 @@ class ECommerceSettings(Document): frappe.throw(_("Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'").format(f.idx, f.fieldname)) def validate_attribute_filters(self): - if not (self.enable_attribute_filters and self.filter_attributes): return + if not (self.enable_attribute_filters and self.filter_attributes): + return # if attribute filters are enabled, hide_variants should be disabled self.hide_variants = 0 diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py index 84a1274d4c..637194e6b8 100644 --- a/erpnext/e_commerce/doctype/item_review/item_review.py +++ b/erpnext/e_commerce/doctype/item_review/item_review.py @@ -4,9 +4,6 @@ from __future__ import unicode_literals from datetime import datetime -from six import string_types -import json - import frappe from frappe.model.document import Document from frappe.contacts.doctype.contact.contact import get_contact_name diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index a7c56cf681..3cff8ec28b 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -48,6 +48,7 @@ class WebsiteItem(WebsiteGenerator): self.update_template_item() def on_trash(self): + super(WebsiteItem, self).on_trash() self.publish_unpublish_desk_item(publish=False) def validate_duplicate_website_item(self): @@ -169,8 +170,8 @@ class WebsiteItem(WebsiteGenerator): context.parents = get_parent_item_groups(self.item_group, from_item=True) self.attributes = frappe.get_all("Item Variant Attribute", - fields=["attribute", "attribute_value"], - filters={"parent": self.item_code}) + fields=["attribute", "attribute_value"], + filters={"parent": self.item_code}) self.set_variant_context(context) self.set_attribute_context(context) self.set_disabled_attributes(context) @@ -181,7 +182,7 @@ class WebsiteItem(WebsiteGenerator): context.wished = False if frappe.db.exists("Wishlist Items", {"item_code": self.item_code, "parent": frappe.session.user}): - context.wished = True + context.wished = True return context @@ -192,8 +193,8 @@ class WebsiteItem(WebsiteGenerator): # load variants # also used in set_attribute_context context.variants = frappe.get_all("Item", - filters={"variant_of": self.name, "show_variant_in_website": 1}, - order_by="name asc") + filters={"variant_of": self.name, "show_variant_in_website": 1}, + order_by="name asc") variant = frappe.form_dict.variant if not variant and context.variants: @@ -227,8 +228,8 @@ class WebsiteItem(WebsiteGenerator): # load attributes for v in context.variants: v.attributes = frappe.get_all("Item Variant Attribute", - fields=["attribute", "attribute_value"], - filters={"parent": v.name}) + fields=["attribute", "attribute_value"], + filters={"parent": v.name}) # make a map for easier access in templates v.attribute_map = frappe._dict({}) for attr in v.attributes: @@ -379,7 +380,8 @@ def invalidate_cache_for_web_item(doc): @frappe.whitelist() def make_website_item(doc, save=True): - if not doc: return + if not doc: + return if isinstance(doc, string_types): doc = json.loads(doc) diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py index eb86dc8f91..c817657a50 100644 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.py +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py @@ -53,7 +53,7 @@ def remove_from_wishlist(item_code): delete from `tabWishlist Items` where item_code=%(item_code)s - """%{"item_code": frappe.db.escape(item_code)}) + """ % {"item_code": frappe.db.escape(item_code)}) frappe.db.commit() diff --git a/erpnext/e_commerce/filters.py b/erpnext/e_commerce/filters.py index 3d28d90558..3ca3d26878 100644 --- a/erpnext/e_commerce/filters.py +++ b/erpnext/e_commerce/filters.py @@ -14,7 +14,8 @@ class ProductFiltersBuilder: self.item_group = item_group def get_field_filters(self): - if not self.item_group and not self.doc.enable_field_filters: return + if not self.item_group and not self.doc.enable_field_filters: + return filter_fields = [row.fieldname for row in self.doc.filter_fields] @@ -48,7 +49,8 @@ class ProductFiltersBuilder: values = [d.name for d in frappe.get_all(doctype, filters)] # Remove None - if None in values: values.remove(None) + if None in values: + values.remove(None) if values: filter_data.append([df, values]) @@ -56,7 +58,8 @@ class ProductFiltersBuilder: return filter_data def get_attribute_filters(self): - if not self.item_group and not self.doc.enable_attribute_filters: return + if not self.item_group and not self.doc.enable_attribute_filters: + return attributes = [row.attribute for row in self.doc.filter_attributes] diff --git a/erpnext/e_commerce/product_configurator/test_product_configurator.py b/erpnext/e_commerce/product_configurator/test_product_configurator.py index 82abee391a..abc1f3052c 100644 --- a/erpnext/e_commerce/product_configurator/test_product_configurator.py +++ b/erpnext/e_commerce/product_configurator/test_product_configurator.py @@ -106,7 +106,7 @@ class TestProductConfigurator(unittest.TestCase): def publish_items_on_website(self): if frappe.db.exists("Item", "_Test Item") and not frappe.db.exists("Website Item", {"item_code": "_Test Item"}): - make_website_item(frappe.get_cached_doc("Item", "_Test Item")) + make_website_item(frappe.get_cached_doc("Item", "_Test Item")) if frappe.db.exists("Item", "_Test Variant Item") and not frappe.db.exists("Website Item", {"item_code": "_Test Variant Item"}): make_website_item(frappe.get_cached_doc("Item", "_Test Variant Item")) diff --git a/erpnext/e_commerce/product_configurator/utils.py b/erpnext/e_commerce/product_configurator/utils.py index 7ccb053adb..9faaa5dc5d 100644 --- a/erpnext/e_commerce/product_configurator/utils.py +++ b/erpnext/e_commerce/product_configurator/utils.py @@ -12,7 +12,8 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None): if not isinstance(attribute_values, list): attribute_values = [attribute_values] - if not attribute_values: continue + if not attribute_values: + continue wheres = [] query_values = [] diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py index 37e91a09e9..de3b0eefb9 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -10,11 +10,11 @@ class ProductQuery: """Query engine for product listing Attributes: - fields (list): Fields to fetch in query - conditions (string): Conditions for query building - or_conditions (string): Search conditions - page_length (Int): Length of page for the query - settings (Document): E Commerce Settings DocType + fields (list): Fields to fetch in query + conditions (string): Conditions for query building + or_conditions (string): Search conditions + page_length (Int): Length of page for the query + settings (Document): E Commerce Settings DocType """ def __init__(self): @@ -31,16 +31,18 @@ class ProductQuery: """Summary Args: - attributes (dict, optional): Item Attribute filters - fields (dict, optional): Field level filters - search_term (str, optional): Search term to lookup - start (int, optional): Page start + attributes (dict, optional): Item Attribute filters + fields (dict, optional): Field level filters + search_term (str, optional): Search term to lookup + start (int, optional): Page start Returns: - list: List of results with set fields + list: List of results with set fields """ - if fields: self.build_fields_filters(fields) - if search_term: self.build_search_filters(search_term) + 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" @@ -72,13 +74,13 @@ class ProductQuery: 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") - ) + 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 @@ -117,7 +119,8 @@ class ProductQuery: self.conditions += " and iva.parent = wi.item_code" for attribute, values in attributes.items(): - if not isinstance(values, list): values = [values] + if not isinstance(values, list): + values = [values] conditions_copy = self.conditions substitutions_copy = self.substitutions.copy() @@ -140,7 +143,7 @@ class ProductQuery: """Build filters for field values Args: - filters (dict): Filters + filters (dict): Filters """ for field, values in filters.items(): if not values: @@ -167,7 +170,7 @@ class ProductQuery: """Query search term in specified fields Args: - search_term (str): Search candidate + search_term (str): Search candidate """ # Default fields to search from default_fields = {'name', 'item_name', 'description', 'item_group'} diff --git a/erpnext/patches/v13_0/make_homepage_products_website_items.py b/erpnext/patches/v13_0/make_homepage_products_website_items.py index 67cccdf023..8b51cad641 100644 --- a/erpnext/patches/v13_0/make_homepage_products_website_items.py +++ b/erpnext/patches/v13_0/make_homepage_products_website_items.py @@ -6,7 +6,8 @@ def execute(): for row in homepage.products: web_item = frappe.db.get_value("Website Item", {"item_code": row.item_code}, "name") - if not web_item: continue + if not web_item: + continue row.item_code = web_item diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index b0371c56bb..0e9e631cec 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -380,7 +380,6 @@ class Item(Document): ) def on_trash(self): - super(Item, self).on_trash() frappe.db.sql("""delete from tabBin where item_code=%s""", self.name) frappe.db.sql("delete from `tabItem Price` where item_code=%s", self.name) for variant_of in frappe.get_all("Item", filters={"variant_of": self.name}): diff --git a/erpnext/templates/pages/customer_reviews.js b/erpnext/templates/pages/customer_reviews.js index 453b96a168..9be12c7bf3 100644 --- a/erpnext/templates/pages/customer_reviews.js +++ b/erpnext/templates/pages/customer_reviews.js @@ -26,7 +26,6 @@ $(() => { {fieldname: "comment", fieldtype: "Small Text", label: "Your Review"} ], primary_action: function() { - let me = this; let data = d.get_values(); frappe.call({ method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review", @@ -39,7 +38,7 @@ $(() => { freeze: true, freeze_message: __("Submitting Review ..."), callback: (r) => { - if(!r.exc) { + if (!r.exc) { frappe.msgprint({ message: __("Thank you for submitting your review"), title: __("Review Submitted"), @@ -74,7 +73,7 @@ $(() => { end: me.page_length }, callback: (result) => { - if(result.message) { + if (result.message) { let res = result.message; me.get_user_review_html(res.reviews); @@ -85,7 +84,7 @@ $(() => { } } - }) + }); }); } @@ -121,7 +120,7 @@ $(() => { get_review_stars(rating) { let stars = ``; - for(let i = 1; i < 6; i++) { + for (let i = 1; i < 6; i++) { let fill_class = i <= rating ? 'star-click' : ''; stars += ` diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py index 0dbcf2188a..96c83da443 100644 --- a/erpnext/templates/pages/wishlist.py +++ b/erpnext/templates/pages/wishlist.py @@ -19,7 +19,7 @@ def get_context(context): "warehouse": item.get("warehouse") }, "actual_qty") - ) + ) item.available = True if stock_qty else False context.items = items @@ -34,5 +34,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.db.escape(frappe.session.user)}, as_dict=1) return \ No newline at end of file From 60261852b291404a032c38e9a2fbb686cbe7ea01 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 13 Apr 2021 00:39:26 +0530 Subject: [PATCH 019/225] feat: Slashed Prices and Discount display - Registered mrp and price after discounts - slashed price with discount in listing, item page and wishlist - removed redundant imports - renamed method to `get_web_item_qty_in_stock` to get Website Item stock - adjusted styles for resizing - made add to cart button full width on cards --- .../e_commerce_settings.py | 2 +- erpnext/e_commerce/product_query.py | 10 ++- erpnext/e_commerce/shopping_cart/cart.py | 4 +- .../e_commerce/shopping_cart/product_info.py | 5 +- erpnext/public/scss/shopping_cart.scss | 14 ++-- erpnext/templates/generators/item/item.html | 2 +- .../generators/item/item_add_to_cart.html | 16 +++- erpnext/templates/includes/macros.html | 83 ++++++++++++------- erpnext/templates/pages/wishlist.html | 2 +- erpnext/templates/pages/wishlist.py | 41 ++++++--- erpnext/utilities/product.py | 32 ++++--- 11 files changed, 138 insertions(+), 73 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index 2de47aa4ea..19de403a54 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -3,7 +3,7 @@ # For license information, please see license.txt import frappe -from frappe.utils import cint +from frappe.utils import cint, comma_and from frappe import _, msgprint from frappe.model.document import Document from frappe.utils import cint diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py index de3b0eefb9..2dbed0af17 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -67,9 +67,13 @@ class ProductQuery: # 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: - item.formatted_price = (product_info.get('price') or {}).get('formatted_price') - item.price = (product_info.get('price') or {}).get('price_list_rate') + 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') if self.settings.show_stock_availability: if item.get("website_warehouse"): diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 011f29cdc6..7abfb42b24 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -11,7 +11,7 @@ from frappe.utils.nestedset import get_root_of from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings from erpnext.accounts.utils import get_account_name -from erpnext.utilities.product import get_qty_in_stock +from erpnext.utilities.product import get_web_item_qty_in_stock class WebsitePriceListMissingError(frappe.ValidationError): @@ -93,7 +93,7 @@ def place_order(): item.item_code, ["website_warehouse", "is_stock_item"]) if is_stock_item: - item_stock = get_qty_in_stock(item.item_code, "website_warehouse") + item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse") if not cint(item_stock.in_stock): throw(_("{1} Not in Stock").format(item.item_code)) if item.qty > item_stock.stock_qty[0][0]: diff --git a/erpnext/e_commerce/shopping_cart/product_info.py b/erpnext/e_commerce/shopping_cart/product_info.py index 352900284d..cd47174b96 100644 --- a/erpnext/e_commerce/shopping_cart/product_info.py +++ b/erpnext/e_commerce/shopping_cart/product_info.py @@ -8,7 +8,7 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( get_shopping_cart_settings, show_quantity_in_website ) -from erpnext.utilities.product import get_price, get_qty_in_stock, get_non_stock_item_status +from erpnext.utilities.product import get_price, get_web_item_qty_in_stock, get_non_stock_item_status @frappe.whitelist(allow_guest=True) def get_product_info_for_website(item_code, skip_quotation_creation=False): @@ -30,8 +30,7 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): cart_settings.default_customer_group, cart_settings.company ) - - stock_status = get_qty_in_stock(item_code, "website_warehouse") + stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse") product_info = { "price": price, diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 905d7e6d49..d7b0cca487 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -68,7 +68,7 @@ body.product-page { .item-card-group-section { .card { - height: 360px; + height: 400px; align-items: center; justify-content: center; @@ -134,6 +134,11 @@ body.product-page { } .item-card { + padding: var(--padding-sm); + min-width: 300px; + } + + .wishlist-card { padding: var(--padding-sm); min-width: 260px; } @@ -626,8 +631,7 @@ body.product-page { .btn-explore-variants { box-shadow: none; margin: var(--margin-sm) 0; - margin-left: 18px; - max-height: 30px; // to avoid resizing on window resize + max-height: 50px; // to avoid resizing on window resize flex: none; transition: 0.3s ease; color: var(--orange-500); @@ -643,13 +647,12 @@ body.product-page { .btn-add-to-cart-list{ box-shadow: none; margin: var(--margin-sm) 0; - max-height: 30px; // to avoid resizing on window resize + max-height: 50px; // to avoid resizing on window resize flex: none; transition: 0.3s ease; } .not-added { - margin-left: 18px; color: var(--blue-500); background-color: white; border: 1px solid var(--blue-500); @@ -661,7 +664,6 @@ body.product-page { } .added-to-cart { - margin-left: 18px; background-color: var(--dark-green-400); color: white; border: 2px solid var(--green-300); diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html index 5f027f7363..427e568aa8 100644 --- a/erpnext/templates/generators/item/item.html +++ b/erpnext/templates/generators/item/item.html @@ -39,7 +39,7 @@ {{ doc.website_content or '' }} - {% if shopping_cart.cart_settings.enable_reviews %} + {% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %} {% 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 3af360f253..97a0480751 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -7,9 +7,21 @@
{% if cart_settings.show_price and product_info.price %} + {% set price_info = product_info.price %} + + {% if price_info.formatted_mrp %} + + M.R.P.: + {{ price_info.formatted_mrp }} + + + {{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}} OFF + + {% endif %} +
- {{ product_info.price.formatted_price_sales_uom }} - ({{ product_info.price.formatted_price }} / {{ product_info.uom }}) + {{ price_info.formatted_price_sales_uom }} + ({{ price_info.formatted_price }} / {{ product_info.uom }})
{% else %} {{ _("UOM") }} : {{ product_info.uom }} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index cd29494e03..106370474e 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -138,35 +138,47 @@ {% endif %}
{% if is_featured %} -
{{ item.formatted_price or '' }}
-
{{ description or '' }}
- {% else %} -
{{ item.item_group or '' }}
-
- {% if item.formatted_price %}
{{ item.formatted_price or '' }}
- {% endif %} - {% if item.has_variants %} - -
- {{ _('Explore') }} +
{{ description or '' }}
+ {% else %} +
{{ item.item_group or '' }}
+ + {% if item.formatted_price %} +
+ {{ item.formatted_price or '' }} + + {% if item.get("formatted_mrp") %} + + {{ item.formatted_mrp }} + + + {{ item.discount }} OFF + + {% endif %} +
-
- {% elif settings.enabled %} -
+
+ {{ _('Explore') }} +
+ + {% elif settings.enabled and (settings.allow_items_not_in_stock or item.in_stock != "red")%} +
{{ _('Add to Cart') }}
{% endif %} -
{% endif %}
{%- endmacro -%} {%- macro wishlist_card(item, settings) %} -
-
+
+
{% if item.image %} {%- endmacro -%} diff --git a/erpnext/templates/pages/wishlist.html b/erpnext/templates/pages/wishlist.html index 6e7a65bcc7..4c039e3c1d 100644 --- a/erpnext/templates/pages/wishlist.html +++ b/erpnext/templates/pages/wishlist.html @@ -7,7 +7,7 @@ {% block page_content %} {% if items %}
-
+
{% from "erpnext/templates/includes/macros.html" import wishlist_card %} {% for item in items %} diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py index 96c83da443..e534a23b9d 100644 --- a/erpnext/templates/pages/wishlist.py +++ b/erpnext/templates/pages/wishlist.py @@ -5,26 +5,45 @@ 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 def get_context(context): settings = frappe.get_doc("E Commerce Settings") items = get_wishlist_items() + selling_price_list = _set_price_list(settings) - 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 + for item in items: + if settings.show_stock_availability: + item.available = get_stock_availability(item.item_code, item.get("warehouse")) + + price_details = get_price( + item.item_code, + selling_price_list, + settings.default_customer_group, + settings.company + ) + + if price_details: + item.formatted_mrp = price_details.get('formatted_mrp') + if item.formatted_mrp: + item.discount = price_details.get('formatted_discount_percent') or \ + price_details.get('formatted_discount_rate') context.items = items context.settings = settings +def get_stock_availability(item_code, warehouse): + stock_qty = frappe.utils.flt( + frappe.db.get_value("Bin", + { + "item_code": item_code, + "warehouse": warehouse + }, + "actual_qty") + ) + return True if stock_qty else False + def get_wishlist_items(): if frappe.db.exists("Wishlist", frappe.session.user): return frappe.db.sql(""" diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index e567f771e9..0b5e9249e8 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -1,24 +1,21 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - import frappe from frappe.utils import cint, flt, fmt_money, getdate, nowdate from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item from erpnext.stock.doctype.batch.batch import get_batch_qty - -def get_qty_in_stock(item_code, item_warehouse_field, warehouse=None): +def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): in_stock, stock_qty = 0, '' template_item_code, is_stock_item = frappe.db.get_value("Item", item_code, ["variant_of", "is_stock_item"]) if not warehouse: - warehouse = frappe.db.get_value("Item", item_code, item_warehouse_field) + 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("Item", 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(""" @@ -93,17 +90,26 @@ def get_price(item_code, price_list, customer_group, company, qty=1): "for_shopping_cart": True, "currency": frappe.db.get_value("Price List", price_list, "currency") })) + price_obj = price[0] if pricing_rule: + # price without any rules applied + mrp = price_obj.price_list_rate or 0 + if pricing_rule.pricing_rule_for == "Discount Percentage": - price[0].price_list_rate = flt(price[0].price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0))) + 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))) if pricing_rule.pricing_rule_for == "Rate": - price[0].price_list_rate = pricing_rule.price_list_rate + rate_discount = flt(mrp) - flt(pricing_rule.price_list_rate) + if rate_discount > 0: + price_obj.formatted_discount_rate = fmt_money(rate_discount, currency=price_obj["currency"]) + price_obj.price_list_rate = pricing_rule.price_list_rate or 0 - price_obj = price[0] if price_obj: price_obj["formatted_price"] = fmt_money(price_obj["price_list_rate"], currency=price_obj["currency"]) + if mrp != price_obj["price_list_rate"]: + price_obj["formatted_mrp"] = fmt_money(mrp, currency=price_obj["currency"]) price_obj["currency_symbol"] = not cint(frappe.db.get_default("hide_currency_symbol")) \ and (frappe.db.get_value("Currency", price_obj.currency, "symbol", cache=True) or price_obj.currency) \ @@ -124,15 +130,15 @@ def get_price(item_code, price_list, customer_group, company, qty=1): price_obj["currency"] = "" if not price_obj["formatted_price"]: - price_obj["formatted_price"] = "" + price_obj["formatted_price"], price_obj["formatted_mrp"] = "", "" return price_obj def get_non_stock_item_status(item_code, item_warehouse_field): - #if item belongs to product bundle, check if bundle items are in stock + # if item is a product bundle, check if its bundle items are in stock 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_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 From 817f695c57b64a27fd8c0f823854e22a286fc083 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 13 Apr 2021 16:57:35 +0530 Subject: [PATCH 020/225] fix: Item schema modified date update - Modified date changed due to merge conflict - Changed it to a later date to make sure Item migrates correctly From 1d949141024e372477de837c5b8cb4a301c1bc98 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 20 Apr 2021 21:54:52 +0530 Subject: [PATCH 021/225] feat: Discount Filters - Discount filters in filters section - Code cleanup --- .../doctype/item_review/item_review.py | 2 +- erpnext/e_commerce/filters.py | 19 +++- erpnext/e_commerce/product_query.py | 73 ++++++++------- erpnext/public/js/wishlist.js | 4 +- .../setup/doctype/item_group/item_group.py | 4 +- erpnext/templates/generators/item_group.html | 69 ++------------- erpnext/templates/includes/macros.html | 88 +++++++++++++++++++ erpnext/utilities/product.py | 5 +- erpnext/www/all-products/index.html | 70 +++------------ erpnext/www/all-products/index.js | 6 +- erpnext/www/all-products/index.py | 9 +- 11 files changed, 186 insertions(+), 163 deletions(-) diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py index 637194e6b8..f19dcf3ee4 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 @@ def get_customer(): 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 3ca3d26878..9ad817c27a 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 @@ class ProductFiltersBuilder: 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 2dbed0af17..c186a05282 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 @@ class ProductQuery: 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 @@ class ProductQuery: # 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 @@ class ProductQuery: 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 b2c840a4d5..6bcb6b14e2 100644 --- a/erpnext/public/js/wishlist.js +++ b/erpnext/public/js/wishlist.js @@ -48,7 +48,7 @@ $.extend(wishlist, { }); 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 @@ $.extend(wishlist, { 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 cdff775f74..9ff9260326 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -93,12 +93,14 @@ class ItemGroup(NestedSet, WebsiteGenerator): 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 233b16974b..a27b566cf3 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 @@
{{ _('Filters') }}
{{ _('Clear All') }}
- {% for field_filter in field_filters %} - {%- set item_field = field_filter[0] %} - {%- set values = field_filter[1] %} -
-
{{ item_field.label }}
+ + {{ field_filter_section(field_filters) }} - {% if values | len > 20 %} - - - {% endif %} + + {{ attribute_filter_section(attribute_filters) }} - {% if values %} -
- {% for value in values %} -
- -
- {% endfor %} -
- {% else %} - {{ _('No values') }} - {% endif %} -
- {% endfor %} - - {% for attribute in attribute_filters %} -
-
{{ attribute.name}}
- {% if values | len > 20 %} - - - {% endif %} - - {% if attribute.item_attribute_values %} -
- {% for attr_value in attribute.item_attribute_values %} -
- -
- {% endfor %} -
- {% else %} - {{ _('No values') }} - {% endif %} -
- {% endfor %} + + {% if discount_filters %} + {{ discount_range_filters(discount_filters) }} + {% endif %}
{% endif %} diff --git a/erpnext/templates/generators/item/item_details.html b/erpnext/templates/generators/item/item_details.html index 95591294a0..cf6e2b9c19 100644 --- a/erpnext/templates/generators/item/item_details.html +++ b/erpnext/templates/generators/item/item_details.html @@ -2,7 +2,7 @@

- {{ item_name }} + {{ doc.web_item_name }}

{{ _("Item Code") }}: diff --git a/erpnext/templates/generators/item/item_reviews.html b/erpnext/templates/generators/item/item_reviews.html index f6b1831cda..fd03a823af 100644 --- a/erpnext/templates/generators/item/item_reviews.html +++ b/erpnext/templates/generators/item/item_reviews.html @@ -1,6 +1,6 @@ {% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %} -

+
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }} diff --git a/erpnext/templates/pages/customer_reviews.html b/erpnext/templates/pages/customer_reviews.html index 9d8ba9e304..e11da3d178 100644 --- a/erpnext/templates/pages/customer_reviews.html +++ b/erpnext/templates/pages/customer_reviews.html @@ -42,4 +42,13 @@
+{% endblock %} + +{% block base_scripts %} + + + + + + {% endblock %} \ No newline at end of file From 8f0b9c9406291a65a8b933d99067abc247dcc4c5 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 10 May 2021 17:28:22 +0530 Subject: [PATCH 037/225] fix: Show Offers section only if offers exist --- .../generators/item/item_add_to_cart.html | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index 9d3ba3b7d4..1da4d15e32 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -45,31 +45,33 @@ {% endif %} -
-

Offers

-
- {% for offer in doc.offers %} -
-
- - - - - - + {% if doc.offers %} +
+

Offers

+
+ {% for offer in doc.offers %} +
+
+ + + + + + +
+

+ {{ _(offer.offer_title) }}: + {{ _(offer.offer_subtitle) }} + + {{ _("More") }} + +

-

- {{ _(offer.offer_title) }}: - {{ _(offer.offer_subtitle) }} - - {{ _("More") }} - -

+ {% endfor %}
- {% endfor %} -
+ {% endif %}
From 48b3ce82b94b3ad9c39f48f5ae9f4126e7db64ac Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 13 May 2021 01:22:05 +0530 Subject: [PATCH 038/225] feat: (wip) Toggle Views - Auto Height on Cards - Title with ellipses on length exceed - Changed namepaces - Moved product card rendering to JS - Added Image and List View Toggling buttons - Kept basic filters rendering just as before --- erpnext/e_commerce/product_query.py | 2 +- erpnext/e_commerce/product_view.js | 124 ++++++++++++++++++ erpnext/public/build.json | 3 + erpnext/public/js/shopping_cart.js | 6 +- erpnext/public/js/wishlist.js | 10 +- erpnext/public/scss/shopping_cart.scss | 5 +- .../setup/doctype/item_group/item_group.py | 26 +--- .../generators/item/item_add_to_cart.html | 6 +- .../generators/item/item_configure.js | 2 +- erpnext/templates/generators/item_group.html | 21 +-- erpnext/templates/includes/cart.js | 4 +- erpnext/templates/includes/macros.html | 36 ++--- erpnext/www/all-products/index.html | 33 ++--- erpnext/www/all-products/index.js | 15 ++- erpnext/www/all-products/index.py | 48 +++++-- 15 files changed, 224 insertions(+), 117 deletions(-) create mode 100644 erpnext/e_commerce/product_view.js diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py index c186a05282..9e675e54d4 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -22,7 +22,7 @@ class ProductQuery: def __init__(self): self.settings = frappe.get_doc("E Commerce Settings") self.page_length = self.settings.products_per_page or 20 - self.fields = ['wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of', + self.fields = ['wi.web_item_name', 'wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of', 'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description', 'wi.route', 'wi.website_warehouse'] self.conditions = "" diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js new file mode 100644 index 0000000000..660db66021 --- /dev/null +++ b/erpnext/e_commerce/product_view.js @@ -0,0 +1,124 @@ +erpnext.ProductView = class { + /* Options: View Type */ + constructor(options) { + Object.assign(this, options); + this.render_view_toggler(); + this.get_item_filter_data(); + this.render_list_view(); + this.render_grid_view(); + } + + render_view_toggler() { + ["btn-list-view", "btn-grid-view"].forEach(view => { + let icon = view === "btn-list-view" ? "list" : "image-view"; + this.products_section.append(` +
+ +
`); + }); + + $("#list").click(function() { + let $btn = $(this); + $btn.removeClass('btn-primary'); + $btn.addClass('btn-primary'); + $(".btn-grid-view").removeClass('btn-primary'); + }) + + $("#image-view").click(function() { + let $btn = $(this); + $btn.removeClass('btn-primary'); + $btn.addClass('btn-primary'); + $(".btn-list-view").removeClass('btn-primary'); + }); + + this.products_area = this.products_section.append(` +

+
+ `); + } + + get_item_filter_data() { + // Get Items and Discount Filters to render + let me = this; + const filters = frappe.utils.get_query_params(); + let {field_filters, attribute_filters} = filters; + + field_filters = field_filters ? JSON.parse(field_filters) : {}; + attribute_filters = attribute_filters ? JSON.parse(attribute_filters) : {}; + + frappe.call({ + method: 'erpnext.www.all-products.index.get_product_filter_data', + args: { + field_filters: field_filters, + attribute_filters: attribute_filters, + item_group: me.item_group + }, + callback: function(result) { + if (!result.exc) { + me.render_filters(result.message[1]); + + // Append pre-rendered products + // TODO: get products as is and style via js + me.products = result.message; + $("#products-area").append(result.message[0]); + + } else { + $("#products-area").append(` +
+ ${__('No products found')} +
`); + + } + } + }); + } + + render_filters(filter_data) { + this.get_discount_filter_html(filter_data.discount_filters); + } + + get_discount_filter_html(filter_data) { + if (filter_data) { + $("#product-filters").append(` +
+
${__("Discounts")}
+
+ `); + + let html = `
`; + filter_data.forEach(filter => { + html += ` +
+ +
+ `; + }); + html += `
`; + + $("#discount-filters").append(html); + } + } + + render_list_view() { + // loop over data and add list html to it + } + + render_grid_view() { + // loop over data and add grid html to it + } + +} \ No newline at end of file diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 6fa3fb9fc3..aa8ef6df6d 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -66,5 +66,8 @@ "js/hierarchy-chart.min.js": [ "public/js/hierarchy_chart/hierarchy_chart_desktop.js", "public/js/hierarchy_chart/hierarchy_chart_mobile.js" + ], + "js/e-commerce.min.js": [ + "e_commerce/product_view.js" ] } diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index b57862b93a..331d04eb43 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -2,8 +2,8 @@ // License: GNU General Public License v3. See license.txt // shopping cart -frappe.provide("erpnext.shopping_cart"); -var shopping_cart = erpnext.shopping_cart; +frappe.provide("e_commerce.shopping_cart"); +var shopping_cart = e_commerce.shopping_cart; var getParams = function (url) { var params = []; @@ -214,7 +214,7 @@ $.extend(shopping_cart, { this.animate_add_to_cart($btn); const item_code = $btn.data('item-code'); - erpnext.shopping_cart.update_cart({ + e_commerce.shopping_cart.update_cart({ item_code, qty: 1 }); diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js index 6bcb6b14e2..11dae355cb 100644 --- a/erpnext/public/js/wishlist.js +++ b/erpnext/public/js/wishlist.js @@ -1,8 +1,8 @@ -frappe.provide("erpnext.wishlist"); -var wishlist = erpnext.wishlist; +frappe.provide("e_commerce.wishlist"); +var wishlist = e_commerce.wishlist; -frappe.provide("erpnext.shopping_cart"); -var shopping_cart = erpnext.shopping_cart; +frappe.provide("e_commerce.shopping_cart"); +var shopping_cart = e_commerce.shopping_cart; $.extend(wishlist, { set_wishlist_count: function() { @@ -79,7 +79,7 @@ $.extend(wishlist, { let me = this; let success_action = function() { - erpnext.wishlist.set_wishlist_count(); + e_commerce.wishlist.set_wishlist_count(); }; if ($wish_icon.hasClass('wished')) { diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 04bf9838a6..acd97add8d 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -68,7 +68,6 @@ body.product-page { .item-card-group-section { .card { - height: 400px; align-items: center; justify-content: center; @@ -779,3 +778,7 @@ body.product-page { padding: 6px; font-size: 14px; } + +#toggle-view { + float: right; +} diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index d944509482..18236802fe 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -65,37 +65,14 @@ class ItemGroup(NestedSet, WebsiteGenerator): self.delete_child_item_groups_key() def get_context(self, context): - context.show_search=True + 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: - search = frappe.form_dict.search - field_filters = frappe.parse_json(frappe.form_dict.field_filters) - attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters) - start = frappe.parse_json(frappe.form_dict.start) - else: - search = None - attribute_filters = None - field_filters = {} - start = 0 - - if not field_filters: - field_filters = {} - - # Ensure the query remains within current item group - field_filters['item_group'] = self.name - - engine = ProductQuery() - 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), @@ -124,6 +101,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): context.no_breadcrumbs = False context.title = self.website_title or self.name + context.name = self.name return context diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index 1da4d15e32..d42453dacd 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -143,7 +143,7 @@ const $btn = $(e.currentTarget); $btn.prop('disabled', true); const item_code = $btn.data('item-code'); - erpnext.shopping_cart.update_cart({ + e_commerce.shopping_cart.update_cart({ item_code, qty: 1, callback(r) { @@ -170,11 +170,11 @@ }; let success_action = function() { $btn.prop('disabled', false); - erpnext.wishlist.set_wishlist_count(); + e_commerce.wishlist.set_wishlist_count(); $('.btn-add-to-wishlist, .btn-view-in-wishlist').toggleClass('hidden'); }; - erpnext.wishlist.add_remove_from_wishlist("add", args, success_action, failure_action); + e_commerce.wishlist.add_remove_from_wishlist("add", args, success_action, failure_action); }); $('.page_content').on('click', '.offer-details', (e) => { diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index 5cb5d15501..d7b8d328c3 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -247,7 +247,7 @@ class ItemConfigure { const additional_notes = Object.keys(this.range_values || {}).map(attribute => { return `${attribute}: ${this.range_values[attribute]}`; }).join('\n'); - erpnext.shopping_cart.update_cart({ + e_commerce.shopping_cart.update_cart({ item_code, additional_notes, qty: 1 diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index a27b566cf3..3ae9a89136 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -16,7 +16,8 @@ {% endblock %} {% block page_content %} -
+
{% if slideshow %} {{ web_block( @@ -33,7 +34,7 @@ {% endif %}
-
+
{% if sub_categories %}
{{ _('Sub Categories') }}
@@ -48,15 +49,9 @@ {% endfor %}
{% endif %} -
- {% if items %} - {% for item in items %} - {% include "erpnext/www/all-products/item_row.html" %} - {% endfor %} - {% else %} - {% include "erpnext/www/all-products/not_found.html" %} - {% endif %} -
+ + +
@@ -70,10 +65,6 @@ {{ attribute_filter_section(attribute_filters) }} - - {% if discount_filters %} - {{ discount_range_filters(discount_filters) }} - {% endif %}
-
+ + + +
-
-
-
-
- {% if frappe.form_dict.start|int > 0 %} - - {% endif %} - {% if items|length >= page_length %} - - {% endif %} -
-
{% else %} - {{ product_image(doc.website_image or doc.image or 'no-image.jpg', alt=doc.website_image_alt or doc.item_name) }} + {{ product_image(doc.website_image or doc.image, alt=doc.website_image_alt or doc.item_name) }} {% endif %} diff --git a/erpnext/templates/includes/cart/cart_items.html b/erpnext/templates/includes/cart/cart_items.html index 226c600e58..de3228b56e 100644 --- a/erpnext/templates/includes/cart/cart_items.html +++ b/erpnext/templates/includes/cart/cart_items.html @@ -31,7 +31,10 @@ {%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %} {% if variant_of %} - {{ _('Variant of') }} {{ variant_of }} + {{ _('Variant of') }} + + {{ variant_of }} + {% endif %}
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 956d8e54a0..a4bba5c8b7 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -9,7 +9,13 @@ {% macro product_image(website_image, css_class="product-image", alt="") %}
- {{ alt }} + {% if website_image %} + {{ alt }} + {% else %} +
+ {{ frappe.utils.get_abbr(alt) or "NA" }} +
+ {% endif %}
{% endmacro %} From 0dadf535c172b4a3ff63519b556363e3abfac426 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 29 Jun 2021 11:22:27 +0530 Subject: [PATCH 059/225] chore: Cleanup Query Engine and Product query API - Resolved merge conflicts in item_group.py - Separate api.py file for product listing backend api - Brought back ORM in query engine, handled missing cases (website item groups, etc) - Return results from API in a descriptive manner, helps keep sanity in JS - On toggling views store view preference in localStorage --- erpnext/e_commerce/api.py | 49 +++++++++ .../doctype/website_item/website_item.py | 49 +-------- .../test_product_configurator.py | 7 +- erpnext/e_commerce/product_query.py | 103 ++++++++++-------- erpnext/e_commerce/product_view.js | 23 ++-- erpnext/www/all-products/index.js | 2 +- 6 files changed, 132 insertions(+), 101 deletions(-) create mode 100644 erpnext/e_commerce/api.py diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py new file mode 100644 index 0000000000..081e8a95a6 --- /dev/null +++ b/erpnext/e_commerce/api.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.utils import cint + +from erpnext.e_commerce.product_query import ProductQuery +from erpnext.e_commerce.filters import ProductFiltersBuilder +from erpnext.setup.doctype.item_group.item_group import get_child_groups + +@frappe.whitelist(allow_guest=True) +def get_product_filter_data(): + """Get pre-rendered filtered products and discount filters on load.""" + if frappe.form_dict: + search = frappe.form_dict.search + field_filters = frappe.parse_json(frappe.form_dict.field_filters) + attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters) + start = cint(frappe.parse_json(frappe.form_dict.start)) if frappe.form_dict.start else 0 + item_group = frappe.form_dict.item_group + else: + search, attribute_filters, item_group = None, None, None + field_filters = {} + start = 0 + + sub_categories = [] + if item_group: + field_filters['item_group'] = item_group + sub_categories = get_child_groups(item_group) + + engine = ProductQuery() + result = engine.query(attribute_filters, field_filters, search_term=search, + start=start, item_group=item_group) + + # discount filter data + filters = {} + discounts = result["discounts"] + + if discounts: + filter_engine = ProductFiltersBuilder() + filters["discount_filters"] = filter_engine.get_discount_filters(discounts) + + return { + "items": result["items"] or [], + "filters": filters, + "settings": engine.settings, + "sub_categories": sub_categories, + "items_count": result["items_count"] + } \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 98d680b347..954ab0f273 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -2,11 +2,9 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe import json import itertools -from six import string_types from frappe import _ from frappe.website.website_generator import WebsiteGenerator @@ -16,13 +14,12 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews -# SEARCH +# SEARCH from erpnext.e_commerce.website_item_indexing import ( - insert_item_to_index, - update_index_for_item, + insert_item_to_index, + update_index_for_item, delete_item_from_index ) -# ----- class WebsiteItem(WebsiteGenerator): website = frappe._dict( @@ -397,7 +394,7 @@ def make_website_item(doc, save=True): if not doc: return - if isinstance(doc, string_types): + if isinstance(doc, str): doc = json.loads(doc) if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}): @@ -419,7 +416,7 @@ def make_website_item(doc, save=True): # Add to search cache insert_item_to_index(website_item) - + return [website_item.name, website_item.web_item_name] def on_doctype_update(): @@ -442,38 +439,4 @@ def check_if_user_is_customer(user=None): customer = link.link_name break - return True if customer else False - -@frappe.whitelist(allow_guest=True) -def get_product_filter_data(): - """Get pre-rendered filtered products and discount filters on load.""" - from erpnext.e_commerce.product_query import ProductQuery - from erpnext.e_commerce.filters import ProductFiltersBuilder - from erpnext.setup.doctype.item_group.item_group import get_child_groups - - if frappe.form_dict: - search = frappe.form_dict.search - field_filters = frappe.parse_json(frappe.form_dict.field_filters) - attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters) - start = cint(frappe.parse_json(frappe.form_dict.start)) if frappe.form_dict.start else 0 - item_group = frappe.form_dict.item_group - else: - search, attribute_filters, item_group = None, None, None - field_filters = {} - start = 0 - - sub_categories = [] - if item_group: - field_filters['item_group'] = item_group - sub_categories = get_child_groups(item_group) - - engine = ProductQuery() - items, discounts = engine.query(attribute_filters, field_filters, search_term=search, start=start) - - # discount filter data - filters = {} - if discounts: - filter_engine = ProductFiltersBuilder() - filters["discount_filters"] = filter_engine.get_discount_filters(discounts) - - return items or [], filters, engine.settings, sub_categories \ No newline at end of file + return True if customer else False \ No newline at end of file diff --git a/erpnext/e_commerce/product_configurator/test_product_configurator.py b/erpnext/e_commerce/product_configurator/test_product_configurator.py index abc1f3052c..cc6aa99cb1 100644 --- a/erpnext/e_commerce/product_configurator/test_product_configurator.py +++ b/erpnext/e_commerce/product_configurator/test_product_configurator.py @@ -95,13 +95,16 @@ class TestProductConfigurator(unittest.TestCase): # check if item is visible in its own Item Group's page engine = ProductQuery() - items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items") + result = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items") + items = result["items"] + self.assertEqual(len(items), 1) self.assertEqual(items[0].item_code, "Portal Item") # check if item is visible in configured foreign Item Group's page engine = ProductQuery() - items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops") + result = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops") + items = result["items"] item_codes = [row.item_code for row in items] def publish_items_on_website(self): diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py index ed3c18b3fb..b368252a4e 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -18,16 +18,14 @@ class ProductQuery: page_length (Int): Length of page for the query settings (Document): E Commerce Settings DocType """ - def __init__(self): self.settings = frappe.get_doc("E Commerce Settings") self.page_length = self.settings.products_per_page or 20 - self.fields = ['wi.web_item_name', 'wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of', - 'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description', - 'wi.route', 'wi.website_warehouse'] - self.conditions = "" - self.or_conditions = "" - self.substitutions = [] + self.fields = ['web_item_name', 'name', 'item_name', 'item_code', 'website_image', + 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', + 'description', 'route', 'website_warehouse', 'ranking'] + self.filters = [["published", "=", 1]] + self.or_filters = [] def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """Summary @@ -42,19 +40,28 @@ class ProductQuery: list: List of results with set fields """ result, discount_list = [], [] + website_item_groups = [] + + # if from item group page consider website item group table + if item_group: + website_item_groups = frappe.db.get_all( + "Website Item", + fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], + filters=[["Website Item Group", "item_group", "=", item_group]] + ) 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" + self.filters.append(["variant_of", "is", "not set"]) + count = 0 if attributes: - result = self.query_items_with_attributes(attributes, start) + result, count = self.query_items_with_attributes(attributes, start) else: - result = self.query_items(self.conditions, self.or_conditions, - self.substitutions, start=start) + result, count = self.query_items(start=start) # add price and availability info in results for item in result: @@ -78,7 +85,11 @@ class ProductQuery: 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 + return { + "items": result, + "items_count": count, + "discounts": discounts + } def get_price_discount_info(self, item, price_object, discount_list): """Modify item object and add price details.""" @@ -104,51 +115,52 @@ class ProductQuery: 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, with_attributes=False): + def query_items(self, start=0): """Build a query to fetch Website Items based on field filters.""" - self.query_fields = ", ".join(self.fields) + count = frappe.db.get_all( + "Website Item", + fields=["count(*) as count"], + filters=self.filters, + or_filters=self.or_filters, + limit_start=start)[0].get("count") - attribute_table = ", `tabItem Variant Attribute` iva" if with_attributes else "" + items = frappe.db.get_all( + "Website Item", + fields=self.fields, + filters=self.filters, + or_filters=self.or_filters, + limit_page_length=self.page_length, + limit_start=start) - return frappe.db.sql(f""" - select distinct {self.query_fields} - from - `tabWebsite Item` wi {attribute_table} - where - wi.published = 1 - {conditions} - {or_conditions} - limit {self.page_length} offset {start} - """, - tuple(substitutions), - as_dict=1) + return items, count or 0 def query_items_with_attributes(self, attributes, start=0): """Build a query to fetch Website Items based on field & attribute filters.""" all_items = [] - self.conditions += " and iva.parent = wi.item_code" + item_codes = [] for attribute, values in attributes.items(): if not isinstance(values, list): values = [values] - conditions_copy = self.conditions - substitutions_copy = self.substitutions.copy() + # get items that have selected attribute & value + item_code_list = frappe.db.get_all( + "Item", + fields=["item_code"], + filters=[ + ["published_in_website", "=", 1], + ["Item Variant Attribute", "attribute", "=", attribute], + ["Item Variant Attribute", "attribute_value", "in", values] + ]) + item_codes.append({x.item_code for x in item_code_list}) - conditions_copy += " and iva.attribute = '{0}' and iva.attribute_value in ({1})" \ - .format(attribute, (", ").join(['%s'] * len(values))) - substitutions_copy.extend(values) + if item_codes: + item_codes = list(set.intersection(*item_codes)) + self.filters.append(["item_code", "in", item_codes]) - items = self.query_items(conditions_copy, self.or_conditions, substitutions_copy, - start=start, with_attributes=True) + items, count = self.query_items(start=start) - items_dict = {item.name: item for item in items} - # TODO: Replace Variants by their parent templates - - all_items.append(set(items_dict.keys())) - - result = [items_dict.get(item) for item in set.intersection(*all_items)] - return result + return items, count def build_fields_filters(self, filters): """Build filters for field values @@ -171,11 +183,10 @@ class ProductQuery: self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) elif isinstance(values, list): # If value is a list use `IN` query - self.conditions += " and wi.{0} in ({1})".format(field, (', ').join(['%s'] * len(values))) - self.substitutions.extend(values) + self.filters.append([field, "in", values]) else: # `=` will be faster than `IN` for most cases - self.conditions += " and wi.{0} = '{1}'".format(field, values) + self.filters.append([field, "=", values]) def build_search_filters(self, search_term): """Query search term in specified fields @@ -198,4 +209,4 @@ class ProductQuery: # Build or filters for query search = '%{}%'.format(search_term) for field in search_fields: - self.or_conditions += " or {0} like '{1}'".format(field, search) + self.or_filters.append([field, "like", search]) diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js index 5ebf44ec5c..67fc91395b 100644 --- a/erpnext/e_commerce/product_view.js +++ b/erpnext/e_commerce/product_view.js @@ -32,28 +32,31 @@ erpnext.ProductView = class { this.disable_view_toggler(true); frappe.call({ - method: 'erpnext.e_commerce.doctype.website_item.website_item.get_product_filter_data', + method: 'erpnext.e_commerce.api.get_product_filter_data', args: args, callback: function(result) { if (!result.exc && result && result.message) { - if (me.item_group && result.message[3].length) { - me.render_item_sub_categories(result.message[3]); + if (me.item_group && result.message["sub_categories"].length) { + me.render_item_sub_categories(result.message["sub_categories"]); } - if (!result.message[0].length) { + if (!result.message["items"].length) { // if result has no items or result is empty me.render_no_products_section(); + + me.bind_filters(); + me.restore_filters_state(); } else { - me.render_filters(result.message[1]); + me.render_filters(result.message["filters"]); // Render views - me.render_list_view(result.message[0], result.message[2]); - me.render_grid_view(result.message[0], result.message[2]); - me.products = result.message[0]; + me.render_list_view(result.message["items"], result.message["settings"]); + me.render_grid_view(result.message["items"], result.message["settings"]); + me.products = result.message["items"]; } // Bottom paging - me.add_paging_section(result.message[2]); + me.add_paging_section(result.message["settings"]); } else { me.render_no_products_section(); } @@ -190,6 +193,7 @@ erpnext.ProductView = class { $("#products-grid-area").addClass("hidden"); $("#products-list-area").removeClass("hidden"); + localStorage.setItem("product_view", "List View"); }); $("#image-view").click(function() { @@ -200,6 +204,7 @@ erpnext.ProductView = class { $("#products-list-area").addClass("hidden"); $("#products-grid-area").removeClass("hidden"); + localStorage.setItem("product_view", "Grid View"); }); } diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index e38514a32a..db2dec046d 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -5,7 +5,7 @@ $(() => { let is_item_group_page = $(".item-group-content").data("item-group"); this.item_group = is_item_group_page || null; - let view_type = "List View"; + let view_type = localStorage.getItem("product_view") || "List View"; // Render Product Views, Filters & Search frappe.require('/assets/js/e-commerce.min.js', function() { From b450f1c583eafd9a1bff53179dd35f97efa06bb1 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 7 Jul 2021 16:30:56 +0530 Subject: [PATCH 060/225] fix: Filters state, Search input clearing, Paging buttons - Fixed repetitive calls on checking filter checkbox - Query count of items after offset for accurate Paging button display - Order items by ranking in query - Search results get empty on clearing input --- erpnext/e_commerce/product_query.py | 16 ++++++++---- erpnext/e_commerce/product_search.js | 9 +++++-- erpnext/e_commerce/product_view.js | 39 ++++++++++++++++------------ 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py index b368252a4e..476c6401b4 100644 --- a/erpnext/e_commerce/product_query.py +++ b/erpnext/e_commerce/product_query.py @@ -117,12 +117,17 @@ class ProductQuery: def query_items(self, start=0): """Build a query to fetch Website Items based on field filters.""" - count = frappe.db.get_all( + # MySQL does not support offset without limit, + # frappe does not accept two parameters for limit + # https://dev.mysql.com/doc/refman/8.0/en/select.html#id4651989 + count_items = frappe.db.get_all( "Website Item", - fields=["count(*) as count"], filters=self.filters, or_filters=self.or_filters, - limit_start=start)[0].get("count") + limit_page_length=184467440737095516, + limit_start=start, # get all items from this offset for total count ahead + order_by="ranking desc") + count = len(count_items) items = frappe.db.get_all( "Website Item", @@ -130,9 +135,10 @@ class ProductQuery: filters=self.filters, or_filters=self.or_filters, limit_page_length=self.page_length, - limit_start=start) + limit_start=start, + order_by="ranking desc") - return items, count or 0 + return items, count def query_items_with_attributes(self, attributes, start=0): """Build a query to fetch Website Items based on field & attribute filters.""" diff --git a/erpnext/e_commerce/product_search.js b/erpnext/e_commerce/product_search.js index e537688199..8466b4f58c 100644 --- a/erpnext/e_commerce/product_search.js +++ b/erpnext/e_commerce/product_search.js @@ -39,6 +39,11 @@ erpnext.ProductSearch = class { this.searchBox.on("input", (e) => { let query = e.target.value; + if (query.length == 0) { + me.populateResults([]); + me.populateCategoriesList([]); + } + if (query.length < 3 || !query.length) return; // Populate recent search chips @@ -191,7 +196,7 @@ erpnext.ProductSearch = class { } populateResults(data) { - if (data.message.results.length === 0) { + if (data.length === 0 || data.message.results.length === 0) { let empty_html = `
${ __('No results') } @@ -220,7 +225,7 @@ erpnext.ProductSearch = class { } populateCategoriesList(data) { - if (data.message.results.length === 0) { + if (data.length === 0 || data.message.results.length === 0) { let empty_html = ` ${__('No results')} diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js index 67fc91395b..c083991dc1 100644 --- a/erpnext/e_commerce/product_view.js +++ b/erpnext/e_commerce/product_view.js @@ -10,10 +10,10 @@ erpnext.ProductView = class { this.make(); } - make() { + make(from_filters=false) { this.products_section.empty(); this.prepare_view_toggler(); - this.get_item_filter_data(); + this.get_item_filter_data(from_filters); } prepare_view_toggler() { @@ -24,7 +24,7 @@ erpnext.ProductView = class { } } - get_item_filter_data() { + get_item_filter_data(from_filters=false) { // Get and render all Product related views let me = this; let args = this.get_query_filters(); @@ -43,16 +43,25 @@ erpnext.ProductView = class { if (!result.message["items"].length) { // if result has no items or result is empty me.render_no_products_section(); - - me.bind_filters(); - me.restore_filters_state(); } else { - me.render_filters(result.message["filters"]); + // Add discount filters + me.get_discount_filter_html(result.message["filters"].discount_filters); // Render views me.render_list_view(result.message["items"], result.message["settings"]); me.render_grid_view(result.message["items"], result.message["settings"]); + me.products = result.message["items"]; + me.product_count = result.message["items_count"]; + } + + // Bind filter actions + if (!from_filters) { + // If `get_product_filter_data` was triggered after checking a filter, + // don't touch filters unnecessarily, only data must change + // filter persistence is handle on filter change event + me.bind_filters(); + me.restore_filters_state(); } // Bottom paging @@ -71,12 +80,6 @@ erpnext.ProductView = class { $('#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) { // loop over data and add grid html to it let me = this; @@ -143,10 +146,9 @@ erpnext.ProductView = class { let query_params = frappe.utils.get_query_params(); let start = query_params.start ? cint(JSON.parse(query_params.start)) : 0; let page_length = settings.products_per_page || 0; - let no_of_products = this.products.length; let prev_disable = start > 0 ? "" : "disabled"; - let next_disable = (no_of_products > page_length || no_of_products == page_length) ? "" : "disabled"; + let next_disable = (this.product_count > page_length) ? "" : "disabled"; paging_html += ` +
`; }); @@ -197,11 +188,7 @@ erpnext.ProductSearch = class { populateResults(data) { if (data.length === 0 || data.message.results.length === 0) { - let empty_html = ` -
- ${ __('No results') } -
- `; + let empty_html = ``; this.products_container.html(empty_html); return; } @@ -228,21 +215,27 @@ erpnext.ProductSearch = class { populateCategoriesList(data) { if (data.length === 0 || data.message.results.length === 0) { let empty_html = ` - - ${__('No results')} - +
+
+
+
`; this.category_container.html(empty_html); return; } - let html = ""; + let html = ` +
+ ${ __("Categories") } +
+ `; let search_results = data.message.results; search_results.forEach((category) => { html += ` - + + ${ category.name } + `; }); diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js index a70be44de4..9f540001c7 100644 --- a/erpnext/e_commerce/product_view.js +++ b/erpnext/e_commerce/product_view.js @@ -12,11 +12,25 @@ erpnext.ProductView = class { make(from_filters=false) { this.products_section.empty(); - this.prepare_view_toggler(); + this.prepare_toolbar(); this.get_item_filter_data(from_filters); } + prepare_toolbar() { + this.products_section.append(` +
+
+ `); + this.prepare_search(); + this.prepare_view_toggler(); + + frappe.require('/assets/js/e-commerce.min.js', function() { + new erpnext.ProductSearch(); + }); + } + prepare_view_toggler() { + if (!$("#list").length || !$("#image-view").length) { this.render_view_toggler(); this.bind_view_toggler_actions(); @@ -173,19 +187,45 @@ erpnext.ProductView = class { } } + prepare_search() { + $(".toolbar").append(` +
+ +
+ `); + } + render_view_toggler() { + $(".toolbar").append(`
`); + ["btn-list-view", "btn-grid-view"].forEach(view => { let icon = view === "btn-list-view" ? "list" : "image-view"; - this.products_section.append(` -
- -
`); + $(".toggle-container").append(` +
+ +
+ `); }); } @@ -448,9 +488,6 @@ erpnext.ProductView = class { render_item_sub_categories(categories) { if (categories && categories.length) { let sub_group_html = ` -
-
${ __('Sub Categories') }
-
`; diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index 31d34bcc64..be0c21f0ce 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -166,19 +166,6 @@ $.extend(shopping_cart, { }); }, - 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); @@ -189,7 +176,10 @@ $.extend(shopping_cart, { const $btn = $(e.currentTarget); $btn.prop('disabled', true); - this.animate_add_to_cart($btn); + $btn.addClass('hidden'); + $btn.parent().find('.go-to-cart').removeClass('hidden'); + $btn.parent().find('.go-to-cart-grid').removeClass('hidden'); + $btn.parent().find('.cart-indicator').removeClass('hidden'); const item_code = $btn.data('item-code'); e_commerce.shopping_cart.update_cart({ diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js index 984572383a..4333587dc6 100644 --- a/erpnext/public/js/wishlist.js +++ b/erpnext/public/js/wishlist.js @@ -79,7 +79,7 @@ $.extend(wishlist, { bind_wishlist_action() { // 'wish'('like') or 'unwish' item in product listing - $('.page_content').on('click', '.like-action', (e) => { + $('.page_content').on('click', '.like-action, .like-action-list', (e) => { const $btn = $(e.currentTarget); const $wish_icon = $btn.find('.wish-icon'); let me = this; @@ -101,6 +101,7 @@ $.extend(wishlist, { if ($wish_icon.hasClass('wished')) { // un-wish item $btn.removeClass("like-animate"); + $btn.addClass("like-action-wished"); this.toggle_button_class($wish_icon, 'wished', 'not-wished'); let args = { item_code: $btn.data('item-code') }; @@ -111,6 +112,7 @@ $.extend(wishlist, { } else { // wish item $btn.addClass("like-animate"); + $btn.addClass("like-action-wished"); this.toggle_button_class($wish_icon, 'not-wished', 'wished'); let args = { diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 5f0f0230cd..27270f3517 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -1,5 +1,9 @@ @import "frappe/public/scss/common/mixins"; +:root { + --green-info: #38A160; +} + body.product-page { background: var(--gray-50); } @@ -80,6 +84,7 @@ body.product-page { .item-card-group-section { .card { + height: 100%; align-items: center; justify-content: center; @@ -89,6 +94,19 @@ body.product-page { } } + .card:hover, .card:focus-within { + .btn-add-to-cart-list { + visibility: visible; + } + .like-action { + visibility: visible; + } + .btn-explore-variants { + visibility: visible; + } + } + + .card-img-container { height: 210px; width: 100%; @@ -102,12 +120,10 @@ body.product-page { .no-image { @include flex(flex, center, center, null); - height: 200px; - margin: 0 auto; - margin-top: var(--margin-xl); + height: 220px; background: var(--gray-100); - width: 80%; - border-radius: var(--border-radius); + width: 100%; + border-radius: var(--border-radius) var(--border-radius) 0 0; font-size: 2rem; color: var(--gray-500); } @@ -123,6 +139,11 @@ body.product-page { margin-bottom: 15px; } + .card-body-flex { + display: flex; + flex-direction: column; + } + .product-title { font-size: 14px; color: var(--gray-800); @@ -153,6 +174,24 @@ body.product-page { font-weight: 600; color: var(--text-color); margin: var(--margin-sm) 0; + + .striked-price { + font-weight: 500; + font-size: 15px; + color: var(--gray-500); + } + } + + .product-info-green { + color: var(--green-info); + font-weight: 600; + } + + .out-of-stock { + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: #F47A7A; } .item-card { @@ -166,6 +205,28 @@ body.product-page { } } +.list-row { + padding-bottom: 1rem; + padding-top: 1.5rem !important; + border-radius: 8px; + border-bottom: 1px solid var(--gray-50); + + &:hover, &:focus-within { + box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04); + transition: box-shadow 400ms; + + .btn-add-to-cart-list { + visibility: visible; + } + .like-action-list { + visibility: visible; + } + .btn-explore-variants { + visibility: visible; + } + } +} + [data-doctype="Item Group"], #page-all-products { .page-header { @@ -220,6 +281,7 @@ body.product-page { } .list-image { + border: none !important; overflow: hidden; max-height: 200px; background-color: white; @@ -341,7 +403,7 @@ body.product-page { .product-code { color: var(--text-muted); - font-size: 13px; + font-size: 14px; } .item-configurator-dialog { @@ -391,7 +453,7 @@ body.product-page { } .sub-category-container { - padding-bottom: 1rem; + padding-bottom: .5rem; margin-bottom: 1.25rem; border-bottom: 1px solid var(--table-border-color); @@ -668,14 +730,68 @@ body.product-page { } } -.card-indicator { - margin-left: 6px; +.cart-indicator { + position: absolute; + text-align: center; + width: 22px; + height: 22px; + left: calc(100% - 40px); + top: 22px; + + border-radius: 66px; + box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1); + background: white; + color: var(--primary-color); + font-size: 14px; } + .like-action { + visibility: hidden; text-align: center; - margin-top: -2px; - margin-left: 12px; + position: absolute; + cursor: pointer; + width: 28px; + height: 28px; + left: 20px; + top: 20px; + + /* White */ + background: white; + box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1); + border-radius: 66px; + + &.like-action-wished { + visibility: visible !important; + } + + @media (max-width: 992px) { + visibility: visible !important; + } +} + +.like-action-list { + visibility: hidden; + text-align: center; + position: absolute; + cursor: pointer; + width: 28px; + height: 28px; + left: 20px; + top: 0; + + /* White */ + background: white; + box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1); + border-radius: 66px; + + &.like-action-wished { + visibility: visible !important; + } + + @media (max-width: 992px) { + visibility: visible !important; + } } .like-animate { @@ -684,21 +800,19 @@ body.product-page { @keyframes expand { 30% { - transform: scale(1.6); + transform: scale(1.3); } 50% { transform: scale(0.8); } 70% { - transform: scale(1.3); + transform: scale(1.1); } 100% { transform: scale(1); } } -@keyframes heart { 0%, 17.5% { font-size: 0; } } - .not-wished { cursor: pointer; stroke: #F47A7A !important; @@ -725,52 +839,52 @@ body.product-page { } .btn-explore-variants { + visibility: hidden; box-shadow: none; margin: var(--margin-sm) 0; width: 90px; max-height: 50px; // to avoid resizing on window resize flex: none; transition: 0.3s ease; - color: var(--orange-500); - background-color: white; + + color: white; + background-color: var(--orange-500); border: 1px solid var(--orange-500); font-size: 13px; &:hover { color: white; - background-color: var(--orange-500); } } .btn-add-to-cart-list{ + visibility: hidden; box-shadow: none; margin: var(--margin-sm) 0; + margin-top: auto !important; max-height: 50px; // to avoid resizing on window resize flex: none; transition: 0.3s ease; -} -.not-added { - color: var(--primary-color); - background-color: transparent; - border: 1px solid var(--blue-500); - font-size: 13px; - - &:hover { - background-color: var(--primary-color); - color: white; - } -} - -.added-to-cart { - background-color: var(--dark-green-400); - color: white; - border: 2px solid var(--green-300); font-size: 13px; &:hover { color: white; } + + @media (max-width: 992px) { + visibility: visible !important; + } +} + +.go-to-cart-grid { + max-height: 30px; + margin-top: 1rem !important; +} + +.go-to-cart { + max-height: 30px; + float: right; } .wishlist-cart-not-added { @@ -882,6 +996,41 @@ body.product-page { font-size: 14px; } +#search-results-container { + padding: .25rem 1rem; + + .category-chip { + background-color: var(--gray-100); + border: none !important; + box-shadow: none; + } + + .recent-search { + padding: .5rem .5rem; + border-radius: var(--border-radius); + + &:hover { + background-color: var(--gray-100); + } + } +} + +#search-box { + padding-left: 2.5rem; +} + +.search-icon { + position: absolute; + left: 0; + top: 0; + width: 2.5rem; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + padding-bottom: 1px; +} + #toggle-view { float: right; } diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index d8ae7632ee..51bce7442d 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -101,6 +101,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): context.no_breadcrumbs = False context.title = self.website_title or self.name context.name = self.name + context.item_group_name = self.item_group_name return context diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index a2b0f59089..e099cdde6a 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -2,18 +2,7 @@ {% extends "templates/web.html" %} {% block header %} -
-
{{ title }}
- -
- -
-
+
{{ _(item_group_name) }}
{% endblock header %} {% block script %} diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html index cb5277df3e..3d5517c5f8 100644 --- a/erpnext/www/all-products/index.html +++ b/erpnext/www/all-products/index.html @@ -1,20 +1,9 @@ {% 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 title %}{{ _('All Products') }}{% endblock %} {% block header %} -
-
{{ _('Products') }}
- -
- -
-
+
{{ _('All Products') }}
{% endblock header %} {% block page_content %} diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index db2dec046d..68d60b2606 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -9,8 +9,6 @@ $(() => { // Render Product Views, Filters & Search frappe.require('/assets/js/e-commerce.min.js', function() { - new erpnext.ProductSearch(); - new erpnext.ProductView({ view_type: view_type, products_section: $('#product-listing'), From 2fec068aff4d8be2e2cf4090061877c1fdba6ad1 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 13 Jul 2021 23:46:24 +0530 Subject: [PATCH 066/225] feat: Recommended Items and Item full page refresh - Added Optional Recommended Items - Item Full Page minor UI Refresh - Floating wishlist button in item full page - Reviews section UI Refresh --- .../e_commerce_settings.json | 14 +- .../doctype/recommended_items/__init__.py | 0 .../recommended_items/recommended_items.json | 87 ++++++++ .../recommended_items/recommended_items.py | 8 + .../doctype/website_item/website_item.json | 20 +- .../doctype/website_item/website_item.py | 40 ++++ erpnext/e_commerce/website_item_indexing.py | 1 + erpnext/public/js/shopping_cart.js | 10 + erpnext/public/js/wishlist.js | 90 +++++---- erpnext/public/scss/shopping_cart.scss | 189 ++++++++++++++++-- erpnext/templates/generators/item/item.html | 74 ++++--- .../generators/item/item_add_to_cart.html | 122 ++++------- .../generators/item/item_details.html | 87 +++++--- .../generators/item/item_inquiry.html | 2 +- .../generators/item/item_reviews.html | 38 ++-- .../generators/item/item_specifications.html | 10 +- erpnext/templates/includes/macros.html | 134 +++++++++---- erpnext/templates/pages/customer_reviews.html | 36 ++-- erpnext/templates/pages/customer_reviews.py | 2 + 19 files changed, 690 insertions(+), 274 deletions(-) create mode 100644 erpnext/e_commerce/doctype/recommended_items/__init__.py create mode 100644 erpnext/e_commerce/doctype/recommended_items/recommended_items.json create mode 100644 erpnext/e_commerce/doctype/recommended_items/recommended_items.py 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 232a0612cc..8eeaf53015 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 @@ -27,6 +27,8 @@ "enable_wishlist", "column_break_22", "enable_reviews", + "column_break_23", + "enable_recommendations", "section_break_18", "company", "price_list", @@ -367,12 +369,22 @@ { "fieldname": "column_break_22", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enable_recommendations", + "fieldtype": "Check", + "label": "Enable Recommendations" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-07-07 21:32:17.363276", + "modified": "2021-07-13 16:30:14.715949", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/doctype/recommended_items/__init__.py b/erpnext/e_commerce/doctype/recommended_items/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/e_commerce/doctype/recommended_items/recommended_items.json b/erpnext/e_commerce/doctype/recommended_items/recommended_items.json new file mode 100644 index 0000000000..06ac3dc03b --- /dev/null +++ b/erpnext/e_commerce/doctype/recommended_items/recommended_items.json @@ -0,0 +1,87 @@ +{ + "actions": [], + "creation": "2021-07-12 20:52:12.503470", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "website_item", + "website_item_name", + "column_break_2", + "item_code", + "more_information_section", + "route", + "column_break_6", + "website_item_image", + "website_item_thumbnail" + ], + "fields": [ + { + "fieldname": "website_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Website Item", + "options": "Website Item" + }, + { + "fetch_from": "website_item.web_item_name", + "fieldname": "website_item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Website Item Name", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "fetch_from": "website_item.route", + "fieldname": "route", + "fieldtype": "Small Text", + "label": "Route", + "read_only": 1 + }, + { + "fetch_from": "website_item.image", + "fieldname": "website_item_image", + "fieldtype": "Attach", + "label": "Website Item Image", + "read_only": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fetch_from": "website_item.thumbnail", + "fieldname": "website_item_thumbnail", + "fieldtype": "Data", + "label": "Website Item Thumbnail", + "read_only": 1 + }, + { + "fetch_from": "website_item.item_code", + "fieldname": "item_code", + "fieldtype": "Data", + "label": "Item Code" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-07-13 21:02:19.031652", + "modified_by": "Administrator", + "module": "E-commerce", + "name": "Recommended Items", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/recommended_items/recommended_items.py b/erpnext/e_commerce/doctype/recommended_items/recommended_items.py new file mode 100644 index 0000000000..9782abdec6 --- /dev/null +++ b/erpnext/e_commerce/doctype/recommended_items/recommended_items.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class RecommendedItems(Document): + pass diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index a321584c78..c33cb51ea3 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -39,6 +39,8 @@ "display_additional_information_section", "show_tabbed_section", "tabs", + "recommended_items_section", + "recommended_items", "offers_section", "offers", "section_break_6", @@ -312,13 +314,25 @@ "fieldname": "short_description", "fieldtype": "Small Text", "label": "Short Website Description" + }, + { + "collapsible": 1, + "fieldname": "recommended_items_section", + "fieldtype": "Section Break", + "label": "Recommended Items" + }, + { + "fieldname": "recommended_items", + "fieldtype": "Table", + "label": "Recommended/Similar Items", + "options": "Recommended Items" } ], "has_web_view": 1, "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-07-11 20:49:45.415421", + "modified": "2021-07-12 21:00:04.065803", "modified_by": "Administrator", "module": "E-commerce", "name": "Website Item", @@ -373,10 +387,10 @@ "write": 1 } ], - "search_fields": "item_code, item_name ,item_group", + "search_fields": "web_item_name, item_code, item_group", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "item_name", + "title_field": "web_item_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 3ff58fdf71..081c4bbae6 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -13,6 +13,8 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews +from erpnext.e_commerce.shopping_cart.cart import _set_price_list +from erpnext.utilities.product import get_price # SEARCH from erpnext.e_commerce.website_item_indexing import ( @@ -199,6 +201,12 @@ class WebsiteItem(WebsiteGenerator): context.wished = True context.user_is_customer = check_if_user_is_customer() + + context.recommended_items = None + settings = context.shopping_cart.cart_settings + if settings.enable_recommendations: + context.recommended_items = self.get_recommended_items(settings) + return context def set_variant_context(self, context): @@ -379,6 +387,38 @@ class WebsiteItem(WebsiteGenerator): return tab_values + def get_recommended_items(self, settings): + items = frappe.db.sql(f""" + select + ri.website_item_thumbnail, ri.website_item_name, + ri.route, ri.item_code + from + `tabRecommended Items` ri, `tabWebsite Item` wi + where + ri.item_code = wi.item_code + and ri.parent = '{self.name}' + and wi.published = 1 + order by ri.idx + """, as_dict=1) + + if settings.show_price: + is_guest = frappe.session.user == "Guest" + # Show Price if logged in. + # If not logged in and price is hidden for guest, skip price fetch. + if is_guest and settings.hide_price_for_guest: + return items + + selling_price_list = _set_price_list(settings, None) + for item in items: + item.price_info = get_price( + item.item_code, + selling_price_list, + settings.default_customer_group, + settings.company + ) + + return items + 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/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py index 18dac93ad3..f66d2ef0e8 100644 --- a/erpnext/e_commerce/website_item_indexing.py +++ b/erpnext/e_commerce/website_item_indexing.py @@ -11,6 +11,7 @@ WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict' WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict' ALLOWED_INDEXABLE_FIELDS_SET = { + 'web_item_name', 'item_code', 'item_name', 'item_group', diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index be0c21f0ce..4c134e2dff 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -176,6 +176,16 @@ $.extend(shopping_cart, { const $btn = $(e.currentTarget); $btn.prop('disabled', true); + if (frappe.session.user==="Guest") { + if (localStorage) { + localStorage.setItem("last_visited", window.location.pathname); + } + frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => { + window.location.href = res.message || "/login"; + }); + return; + } + $btn.addClass('hidden'); $btn.parent().find('.go-to-cart').removeClass('hidden'); $btn.parent().find('.go-to-cart-grid').removeClass('hidden'); diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js index 4333587dc6..3c8d842bea 100644 --- a/erpnext/public/js/wishlist.js +++ b/erpnext/public/js/wishlist.js @@ -81,53 +81,55 @@ $.extend(wishlist, { // 'wish'('like') or 'unwish' item in product listing $('.page_content').on('click', '.like-action, .like-action-list', (e) => { const $btn = $(e.currentTarget); - const $wish_icon = $btn.find('.wish-icon'); - let me = this; - - if (frappe.session.user==="Guest") { - if (localStorage) { - localStorage.setItem("last_visited", window.location.pathname); - } - - this.redirect_guest(); - - return; - } - - let success_action = function() { - e_commerce.wishlist.set_wishlist_count(); - }; - - if ($wish_icon.hasClass('wished')) { - // un-wish item - $btn.removeClass("like-animate"); - $btn.addClass("like-action-wished"); - 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"); - $btn.addClass("like-action-wished"); - 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); - } + this.wishlist_action($btn); }); }, + wishlist_action(btn) { + const $wish_icon = btn.find('.wish-icon'); + let me = this; + + if (frappe.session.user==="Guest") { + if (localStorage) { + localStorage.setItem("last_visited", window.location.pathname); + } + this.redirect_guest(); + return; + } + + let success_action = function() { + e_commerce.wishlist.set_wishlist_count(); + }; + + if ($wish_icon.hasClass('wished')) { + // un-wish item + btn.removeClass("like-animate"); + btn.addClass("like-action-wished"); + 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"); + btn.addClass("like-action-wished"); + 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); diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 27270f3517..0130e70302 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -2,6 +2,7 @@ :root { --green-info: #38A160; + --product-bg-color: white; } body.product-page { @@ -289,6 +290,7 @@ body.product-page { .product-container { @include card($padding: var(--padding-md)); + background-color: var(--product-bg-color) !important; min-height: 70vh; .product-details { @@ -299,6 +301,12 @@ body.product-page { } } + &.item-main { + .product-image { + width: 100%; + } + } + .expand { max-width: 100% !important; // expand in absence of slideshow } @@ -327,9 +335,10 @@ body.product-page { } .product-title { - font-size: 24px; + font-size: 16px; font-weight: 600; color: var(--text-color); + padding: 0 !important; } .product-description { @@ -385,7 +394,7 @@ body.product-page { .item-cart { .product-price { - font-size: 20px; + font-size: 22px; color: var(--text-color); font-weight: 600; @@ -398,12 +407,94 @@ body.product-page { .no-stock { font-size: var(--text-base); } + + .offers-heading { + font-size: 16px !important; + color: var(--text-color); + .tag-icon { + --icon-stroke: var(--gray-500); + } + } + + .w-30-40 { + width: 30%; + + @media (max-width: 992px) { + width: 40%; + } + } + } + + .tab-content { + font-size: 14px; + } +} + +// Item Recommendations +.recommended-item-section { + padding-right: 0; + + .recommendation-header { + font-size: 16px; + font-weight: 500 + } + + .recommendation-container { + padding: .5rem; + min-height: 0px; + + .r-item-image { + width: 40%; + + .no-image-r-item { + display: flex; justify-content: center; + background-color: var(--gray-200); + align-items: center; + color: var(--gray-400); + margin-top: .15rem; + border-radius: 6px; + height: 100%; + font-size: 24px; + } + } + + .r-item-info { + font-size: 14px; + padding-left: 8px; + padding-right: 0; + width: 60%; + + a { + color: var(--gray-800); + font-weight: 400; + } + + .item-price { + font-size: 15px; + font-weight: 600; + color: var(--text-color); + } + + .striked-item-price { + font-weight: 500; + color: var(--gray-500); + } + } } } .product-code { + padding: .5rem 0; color: var(--text-muted); font-size: 14px; + .product-item-group { + padding-right: .25rem; + border-right: solid 1px var(--text-muted); + } + + .product-item-code { + padding-left: .5rem; + } } .item-configurator-dialog { @@ -794,6 +885,12 @@ body.product-page { } } +.like-action-item-fp { + visibility: visible !important; + position: unset; + float: right; +} + .like-animate { animation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1; } @@ -919,16 +1016,63 @@ body.product-page { .item-website-specification { font-size: .875rem; + .product-title { + font-size: 18px; + } + + .table { + width: 70%; + } + + td { + border: none !important; + } + + .spec-label { + color: var(--gray-600); + } + + .spec-content { + color: var(--gray-800); + } +} + +.reviews-full-page { + padding: 1rem 2rem; } .ratings-reviews-section { border-top: 1px solid #E2E6E9; + padding: .5rem 1rem; } .reviews-header { font-size: 20px; font-weight: 600; color: var(--gray-800); + display: flex; + align-items: center; + padding: 0; +} + +.btn-write-review { + float: right; + padding: .5rem 1rem; + font-size: 14px; + font-weight: 400; + border: none !important; + box-shadow: none; + + color: var(--gray-900); + background-color: var(--gray-100); + + &:hover { + box-shadow: var(--btn-shadow); + } +} + +.rating-summary-section { + display: flex; } .rating-summary-title { @@ -936,9 +1080,17 @@ body.product-page { font-size: 18px; } +.rating-summary-numbers { + display: flex; + flex-direction: column; + align-items: center; + + border-right: solid 1px var(--gray-100); +} + .user-review-title { margin-top: 0.15rem; - font-size: 16px; + font-size: 15px; font-weight: 600; } @@ -952,6 +1104,12 @@ body.product-page { } } +.ratings-pill { + background-color: var(--gray-100); + padding: .5rem 1rem; + border-radius: 66px; +} + .review { max-width: 80%; line-height: 1.6; @@ -961,21 +1119,18 @@ body.product-page { .review-signature { display: flex; - font-size: 14px; + font-size: 13px; color: var(--gray-500); font-weight: 400; .reviewer { padding-right: 8px; - margin-right: 8px; - border-right: 1px solid var(--gray-400); + color: var(--gray-600); } } .rating-progress-bar-section { padding-bottom: 2rem; - border-bottom: 1px solid #E2E6E9; - margin-right: -10px; .rating-bar-title { margin-left: -15px; @@ -985,14 +1140,15 @@ body.product-page { margin-bottom: 4px; height: 7px; margin-top: 6px; + + .progress-bar-cosmetic { + background-color: var(--gray-600); + border-radius: var(--border-radius); + } } } .offer-container { - border: 1px solid var(--gray-300); - border-style: dashed; - border-radius: 4px; - padding: 6px; font-size: 14px; } @@ -1075,3 +1231,12 @@ body.product-page { .font-md { font-size: 14px !important; } + +.in-green { + color: var(--green-info) !important; + font-weight: 500; +} + +.mt-minus-2 { + margin-top: -2rem; +} diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html index 427e568aa8..e19cfb0b46 100644 --- a/erpnext/templates/generators/item/item.html +++ b/erpnext/templates/generators/item/item.html @@ -1,4 +1,5 @@ {% extends "templates/web.html" %} +{% from "erpnext/templates/includes/macros.html" import recommended_item_row %} {% block title %} {{ title }} {% endblock %} @@ -9,7 +10,7 @@ {% endblock %} {% block page_content %} -
+
{% from "erpnext/templates/includes/macros.html" import product_image %}
@@ -18,33 +19,56 @@ {% include "templates/generators/item/item_image.html" %} {% include "templates/generators/item/item_details.html" %}
- - - {% if show_tabs and tabs %} -
- - {{ web_block( - "Section with Tabs", - values=tabs, - add_container=0, - add_top_padding=0, - add_bottom_padding=0 - ) }} -
- {% elif website_specifications %} - {% include "templates/generators/item/item_specifications.html"%} - {% endif %} - - - {{ doc.website_content or '' }} - - - {% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %} - {% include "templates/generators/item/item_reviews.html"%} - {% endif %}
+ + +
+ {% set show_recommended_items = recommended_items and shopping_cart.cart_settings.enable_recommendations %} + {% set info_col = 'col-9' if show_recommended_items else 'col-12' %} + + {% set padding_top = 'pt-0' if (show_tabs and tabs) else '' %} + +
+
+
+ + {% if show_tabs and tabs %} +
+ + {{ web_block("Section with Tabs", values=tabs, add_container=0, + add_top_padding=0, add_bottom_padding=0) + }} +
+ {% elif website_specifications %} + {% include "templates/generators/item/item_specifications.html"%} + {% endif %} + + + {{ doc.website_content or '' }} + + + {% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %} + {% include "templates/generators/item/item_reviews.html"%} + {% endif %} +
+
+
+ + + {% if show_recommended_items %} + + {% endif %} + +
{% endblock %} {% block base_scripts %} diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index fd243f5059..d52168e657 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -7,34 +7,39 @@
{% if cart_settings.show_price and product_info.price %} - {% set price_info = product_info.price %} + {% set price_info = product_info.price %} - {% if price_info.formatted_mrp %} - - M.R.P.: - {{ price_info.formatted_mrp }} - - - {{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}} OFF - - {% endif %} +
+ + {{ price_info.formatted_price_sales_uom }} -
- {{ price_info.formatted_price_sales_uom }} - ({{ price_info.formatted_price }} / {{ product_info.uom }}) -
+ + {% if price_info.formatted_mrp %} + + MRP {{ price_info.formatted_mrp }} + + + -{{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}} + + {% endif %} + + + + ({{ price_info.formatted_price }} / {{ product_info.uom }}) + +
{% else %} {{ _("UOM") }} : {{ product_info.uom }} {% endif %} {% if cart_settings.show_stock_availability %} -
+
{% if product_info.in_stock == 0 %} {{ _('Not in stock') }} {% elif product_info.in_stock == 1 %} - + {{ _('In stock') }} {% if product_info.show_stock_qty and product_info.stock_qty %} ({{ product_info.stock_qty[0][0] }}) @@ -47,10 +52,15 @@ {% if doc.offers %}
-

Offers

+
+ + + + Available Offers +
{% for offer in doc.offers %} -
+
@@ -59,8 +69,8 @@
-

- {{ _(offer.offer_title) }}: +

+ {{ _(offer.offer_title) }}: {{ _(offer.offer_subtitle) }} -

@@ -155,28 +129,6 @@ }); }); - $('.page_content').on('click', '.btn-add-to-wishlist', (e) => { - // Bind action on wishlist button - const $btn = $(e.currentTarget); - $btn.prop('disabled', true); - - let args = { - item_code: $btn.data('item-code'), - price: $btn.data('price'), - formatted_price: $btn.data('formatted-price') - }; - let failure_action = function() { - $btn.prop('disabled', false); - }; - let success_action = function() { - $btn.prop('disabled', false); - e_commerce.wishlist.set_wishlist_count(); - $('.btn-add-to-wishlist, .btn-view-in-wishlist').toggleClass('hidden'); - - }; - e_commerce.wishlist.add_remove_from_wishlist("add", args, success_action, failure_action); - }); - $('.page_content').on('click', '.offer-details', (e) => { // Bind action on More link in Offers const $btn = $(e.currentTarget); diff --git a/erpnext/templates/generators/item/item_details.html b/erpnext/templates/generators/item/item_details.html index cf6e2b9c19..307e0fed15 100644 --- a/erpnext/templates/generators/item/item_details.html +++ b/erpnext/templates/generators/item/item_details.html @@ -1,28 +1,65 @@ {% set width_class = "expand" if not slides else "" %} +{% set cart_settings = shopping_cart.cart_settings %} +{% set product_info = shopping_cart.product_info %} +{% set price_info = product_info.get('price') or {} %} +
- -

- {{ doc.web_item_name }} -

-

- {{ _("Item Code") }}: - {{ doc.item_code }} -

-{% if has_variants %} - - {% include "templates/generators/item/item_configure.html" %} -{% else %} - - {% include "templates/generators/item/item_add_to_cart.html" %} -{% endif %} - -
-{% if frappe.utils.strip_html(doc.web_long_description or '') %} - {{ doc.web_long_description | safe }} -{% elif frappe.utils.strip_html(doc.description or '') %} - {{ doc.description | safe }} -{% else %} - {{ _("No description given") }} -{% endif %} -
+
+ +
+ {{ doc.web_item_name }} +
+ + + {% if cart_settings.enable_wishlist %} + + {% endif %} +
+ +

+ + {{ _(doc.item_group) }} + + + {{ _("Item Code") }}: + + {{ doc.item_code }} +

+ {% if has_variants %} + + {% include "templates/generators/item/item_configure.html" %} + {% else %} + + {% include "templates/generators/item/item_add_to_cart.html" %} + {% endif %} + +
+ {% if frappe.utils.strip_html(doc.web_long_description or '') %} + {{ doc.web_long_description | safe }} + {% elif frappe.utils.strip_html(doc.description or '') %} + {{ doc.description | safe }} + {% else %} + {{ "" }} + {% endif %} +
+ +{% block base_scripts %} + + +{% endblock %} + + \ No newline at end of file diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html index 72b41676e7..af636f1582 100644 --- a/erpnext/templates/generators/item/item_inquiry.html +++ b/erpnext/templates/generators/item/item_inquiry.html @@ -1,7 +1,7 @@ {% if shopping_cart and shopping_cart.cart_settings.enabled %} {% set cart_settings = shopping_cart.cart_settings %} {% if cart_settings.show_contact_us_button | int %} - {% endif %} diff --git a/erpnext/templates/generators/item/item_reviews.html b/erpnext/templates/generators/item/item_reviews.html index cd38bf3b14..508dbcebe2 100644 --- a/erpnext/templates/generators/item/item_reviews.html +++ b/erpnext/templates/generators/item/item_reviews.html @@ -1,23 +1,29 @@ {% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %} -
-
- {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }} +
+ +
+
+ {{ _("Customer Reviews") }} +
+ +
+ + {% if frappe.session.user != "Guest" and user_is_customer %} + + {% endif %} +
+
+ + + {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }} - - {% if frappe.session.user != "Guest" and user_is_customer %} - - {% endif %} -
-
-

- {{ _("Reviews") }} -

+
{% if reviews %} {{ user_review(reviews) }} @@ -64,7 +70,7 @@ callback: function(r) { if(!r.exc) { frappe.msgprint({ - message: __("Thank you for submitting your review"), + message: __("Thank you for the review"), title: __("Review Submitted"), indicator: "green" }); diff --git a/erpnext/templates/generators/item/item_specifications.html b/erpnext/templates/generators/item/item_specifications.html index f3957610e9..4a59f83646 100644 --- a/erpnext/templates/generators/item/item_specifications.html +++ b/erpnext/templates/generators/item/item_specifications.html @@ -3,13 +3,15 @@
{% if not show_tabs %} -

Product Details

+
+ Product Details +
{% endif %} - +
{% for d in website_specifications -%} - - + + {%- endfor %}
{{ d.label }}{{ d.description }}{{ d.label }}{{ d.description }}
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 8413bb0eba..2863e1572d 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -7,8 +7,8 @@
{% endmacro %} -{% macro product_image(website_image, css_class="product-image", alt="") %} -
+{% macro product_image(website_image, css_class="product-image", alt="", no_border=False) %} +
{% if website_image %} {{ alt }} {% else %} @@ -208,9 +208,12 @@
{%- endmacro -%} -{%- macro ratings_with_title(avg_rating, title, size, rating_header_class) -%} -
-
+{%- macro ratings_with_title(avg_rating, title, size, rating_header_class, for_summary=False) -%} +
+

+ {{ title }} +

+
{% for i in range(1,6) %} {% set fill_class = 'star-click' if i <= avg_rating else '' %} @@ -218,44 +221,50 @@ {% endfor %}
-

- {{ title }} -

{%- endmacro -%} -{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating)-%} - -

- {{ _("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 +{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=False, total_reviews=None)-%} +
+
+

+ {{ average_rating or 0 }} +

+
+ {{ frappe.utils.cstr(total_reviews) + " " + _("ratings") }}
-
-
-
-
+ + + {% if reviews %} + {% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") if not for_summary else ''%} + {{ ratings_with_title(average_whole_rating, rating_title, "md", "rating-summary-title", for_summary) }} + {% endif %} + +
{{ frappe.utils.cstr(average_rating or 0) + " " + _("out of 5") }}
+
+ + +
+ {% for percent in reviews_per_rating %} +
+ {{ loop.index }} star +
+
+
+
+
+
+
+ {{ percent }}% +
-
- {{ percent }}% -
-
- {% endfor %} + {% endfor %} +
{%- endmacro -%} @@ -264,17 +273,19 @@
{% for review in reviews %}
- {{ ratings_with_title(review.rating, _(review.review_title), "md", "user-review-title") }} + {{ ratings_with_title(review.rating, _(review.review_title), "sm", "user-review-title") }} -
- {{ _(review.customer) }} - {{ review.published_on }} -
-
+

{{ _(review.comment) }}

+ +
+ {{ _(review.customer) }} + + {{ review.published_on }} +
{% endfor %}
@@ -347,3 +358,42 @@
{% endfor %} {%- endmacro -%} + +{%- macro recommended_item_row(item)-%} + +{%- endmacro -%} diff --git a/erpnext/templates/pages/customer_reviews.html b/erpnext/templates/pages/customer_reviews.html index e11da3d178..3621fcfaf9 100644 --- a/erpnext/templates/pages/customer_reviews.html +++ b/erpnext/templates/pages/customer_reviews.html @@ -4,25 +4,30 @@ {% block title %} {{ _("Customer Reviews") }} {% endblock %} {% block page_content %} -
-
-
- {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }} +
+ +
+
+ {{ _("Customer Reviews") }} +
- - {% if frappe.session.user != "Guest" %} - - {% endif %} +
+ + {% if frappe.session.user != "Guest" and user_is_customer %} + + {% endif %} +
+ + {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }} + + -
-

- {{ _("Reviews") }} -

+
{% if reviews %} {{ user_review(reviews) }} @@ -40,7 +45,6 @@ {% endif %}
-
{% endblock %} diff --git a/erpnext/templates/pages/customer_reviews.py b/erpnext/templates/pages/customer_reviews.py index b9c8a013c7..2b8ebff3e8 100644 --- a/erpnext/templates/pages/customer_reviews.py +++ b/erpnext/templates/pages/customer_reviews.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews +from erpnext.e_commerce.doctype.website_item.website_item import check_if_user_is_customer def get_context(context): context.no_cache = 1 @@ -11,4 +12,5 @@ def get_context(context): 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") + context.user_is_customer = check_if_user_is_customer() get_item_reviews(context.web_item, 0, 10, context) From ef4579e4ebd0cb37d2aaa6400d6a4d0376f3d08c Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 14 Jul 2021 01:36:50 +0530 Subject: [PATCH 067/225] chore: Wishlist UI (minor) - Minor wishlist UI refresh, actions on hover, new icon - Increase max length of wishlist card title - Dont fetch outdated price in wishlist - Translate 'out of stock' - Use ORM --- erpnext/e_commerce/product_grid.js | 6 +++--- erpnext/e_commerce/product_list.js | 5 ++++- erpnext/public/scss/shopping_cart.scss | 26 ++++++++-------------- erpnext/templates/includes/macros.html | 30 +++++++++++++------------- erpnext/templates/pages/wishlist.py | 24 ++++++++++++--------- 5 files changed, 45 insertions(+), 46 deletions(-) diff --git a/erpnext/e_commerce/product_grid.js b/erpnext/e_commerce/product_grid.js index d29f62f937..b46e0d983a 100644 --- a/erpnext/e_commerce/product_grid.js +++ b/erpnext/e_commerce/product_grid.js @@ -61,7 +61,7 @@ erpnext.ProductGrid = class { get_card_body_html(item, title, settings) { let body_html = `
-
+
`; body_html += this.get_title(item, title); @@ -76,7 +76,7 @@ erpnext.ProductGrid = class { } - body_html += `
`; // close div on line 50 + body_html += `
`; body_html += `
${ item.item_group || '' }
`; if (item.formatted_price) { @@ -145,7 +145,7 @@ erpnext.ProductGrid = class { get_stock_availability(item, settings) { if (!item.has_variants && !item.in_stock && settings.show_stock_availability) { - return `Out of stock`; + return `${ __("Out of stock") }`; } return ``; } diff --git a/erpnext/e_commerce/product_list.js b/erpnext/e_commerce/product_list.js index 03cef604f2..822a9efe19 100644 --- a/erpnext/e_commerce/product_list.js +++ b/erpnext/e_commerce/product_list.js @@ -126,7 +126,10 @@ erpnext.ProductList = class { get_stock_availability(item, settings) { if (!item.has_variants && !item.in_stock && settings.show_stock_availability) { - return `
Out of stock`; + return ` +
+ ${ __("Out of stock") } + `; } return ``; } diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 0130e70302..e25de9ac6d 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -203,6 +203,10 @@ body.product-page { .wishlist-card { padding: var(--padding-sm); min-width: 260px; + .card-body-flex { + display: flex; + flex-direction: column; + } } } @@ -984,30 +988,18 @@ body.product-page { float: right; } -.wishlist-cart-not-added { - color: var(--blue-500); - background-color: white; - border: 1px solid var(--blue-500); - --icon-stroke: var(--blue-500); - font-size: 14px; - - &:hover { - background-color: var(--blue-500); - color: white; - --icon-stroke: white; - } -} - .remove-wish { - background-color: var(--gray-200); + background-color: white; position: absolute; cursor: pointer; top:10px; right: 20px; + width: 32px; + height: 32px; + border-radius: 50%; border: 1px solid var(--gray-100); - width: 25px; - height: 25px; + box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1); } .wish-removed { diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 2863e1572d..99b71541ba 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -145,9 +145,9 @@ {%- macro wishlist_card(item, settings) %} {%- set title = item.web_item_name or ''-%} -{%- set title = title[:50] + "..." if title|len > 50 else title -%} +{%- set title = title[:90] + "..." if title|len > 90 else title -%}
-
+
@@ -173,18 +171,19 @@ {%- endmacro -%} {%- macro wishlist_card_body(item, title, settings) %} -
-
+
+
{{ title or ''}}
+
{{ item.item_group or '' }}
- {{ item.formatted_price or '' }} + {{ item.get("formatted_price") or '' }} {% if item.get("formatted_mrp") %} - + {{ item.formatted_mrp }} - + {{ item.discount }} OFF {% endif %} @@ -192,7 +191,8 @@ {% if (item.available and settings.show_stock_availability) or (not settings.show_stock_availability) %} - {% else %} -
- {{ _("Not in Stock") }} +
+ {{ _("Out of stock") }}
{% endif %}
diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py index 5992863b67..dfd60f58ec 100644 --- a/erpnext/templates/pages/wishlist.py +++ b/erpnext/templates/pages/wishlist.py @@ -21,6 +21,7 @@ def get_context(context): ) if price_details: + item.formatted_price = price_details.get('formatted_price') item.formatted_mrp = price_details.get('formatted_mrp') if item.formatted_mrp: item.discount = price_details.get('formatted_discount_percent') or \ @@ -42,13 +43,16 @@ def get_stock_availability(item_code, warehouse): return bool(stock_qty) def get_wishlist_items(): - if frappe.db.exists("Wishlist", frappe.session.user): - return frappe.db.sql(""" - Select - web_item_name, item_code, item_name, website_item, price, - warehouse, image, item_group, route, formatted_price - from - `tabWishlist Item` - where - parent=%(user)s""", {"user": frappe.session.user}, as_dict=1) - return \ No newline at end of file + if not frappe.db.exists("Wishlist", frappe.session.user): + return [] + + return frappe.db.get_all( + "Wishlist Item", + filters={ + "parent": frappe.session.user + }, + fields=[ + "web_item_name", "item_code", "item_name", + "website_item", "price", "warehouse", + "image", "item_group", "route" + ]) From 929a24ea5ccaab18f72dd75f3a5c252aa739ebfb Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 15 Jul 2021 20:09:02 +0530 Subject: [PATCH 068/225] feat: Cart minor UI/UX Refresh - Added Setting to show or hide price if checkout is disabled - Show Web Item name in cart instead of Desk Item name - Cart minor UI Refresh: added images in cart - Cart minor UI Refresh: repositioned remove button and redesigned - Cart minor UI Refresh: Payment Summary section - Cart minor UI Refresh: Disable input on free item - Cart minor UI Refresh: Add address button in cards - New file for cart payment summary UI with coupon code (old) --- .../e_commerce_settings.json | 12 +- erpnext/e_commerce/shopping_cart/cart.py | 8 +- erpnext/public/js/shopping_cart.js | 2 +- erpnext/public/scss/shopping_cart.scss | 130 ++++++++++++++---- .../quotation_item/quotation_item.json | 2 +- .../templates/includes/cart/cart_address.html | 56 ++++---- .../templates/includes/cart/cart_items.html | 126 ++++++++++------- .../includes/cart/cart_payment_summary.html | 80 +++++++++++ erpnext/templates/pages/cart.html | 58 +++++--- 9 files changed, 347 insertions(+), 127 deletions(-) create mode 100644 erpnext/templates/includes/cart/cart_payment_summary.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 8eeaf53015..54e88ca081 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 @@ -39,8 +39,9 @@ "quotation_series", "checkout_settings_section", "enable_checkout", - "save_quotations_as_draft", + "show_price_in_quotation", "column_break_27", + "save_quotations_as_draft", "payment_gateway_account", "payment_success_url", "filter_categories_section", @@ -379,12 +380,19 @@ "fieldname": "enable_recommendations", "fieldtype": "Check", "label": "Enable Recommendations" + }, + { + "default": "0", + "depends_on": "eval: doc.enable_checkout == 0", + "fieldname": "show_price_in_quotation", + "fieldtype": "Check", + "label": "Show Price in Quotation" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-07-13 16:30:14.715949", + "modified": "2021-07-15 16:50:50.087281", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index d3b860df96..8b27d2b9c4 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -271,8 +271,12 @@ def guess_territory(): def decorate_quotation_doc(doc): for d in doc.get("items", []): - d.update(frappe.db.get_value("Website Item", {"item_code": d.item_code}, - ["thumbnail", "website_image", "description", "route"], as_dict=True)) + d.update(frappe.db.get_value( + "Website Item", + {"item_code": d.item_code}, + ["web_item_name", "thumbnail", "website_image", "description", "route"], + as_dict=True) + ) return doc diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index 4c134e2dff..847d8c6245 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -121,7 +121,7 @@ $.extend(shopping_cart, { $(".cart-items").html('Cart is Empty'); $(".cart-tax-items").hide(); $(".btn-place-order").hide(); - $(".cart-addresses").hide(); + $(".cart-payment-addresses").hide(); } else { $cart.css("display", "inline"); diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index e25de9ac6d..99db127f85 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -628,6 +628,7 @@ body.product-page { display: flex; flex-direction: column; justify-content: space-between; + height: fit-content; } .cart-items-header { @@ -656,8 +657,29 @@ body.product-page { color: var(--text-color); } + .cart-item-image { + width: 20%; + min-width: 100px; + img { + max-height: 112px; + } + + .no-image-cart-item { + max-height: 112px; + display: flex; justify-content: center; + background-color: var(--gray-200); + align-items: center; + color: var(--gray-400); + margin-top: .15rem; + border-radius: 6px; + height: 100%; + font-size: 24px; + } + } + .cart-items { .item-title { + width: 80%; font-size: 14px; font-weight: 500; color: var(--text-color); @@ -688,9 +710,18 @@ body.product-page { color: var(--text-muted); } - textarea { - width: 40%; + .free-tag { + padding: 4px 8px; + border-radius: 4px; + background-color: var(--dark-green-50); } + + textarea { + width: 80%; + height: 60px; + font-size: 14px; + } + } .cart-tax-items { @@ -715,52 +746,95 @@ body.product-page { } .remove-cart-item { - border-radius: 50%; + border-radius: 6px; border: 1px solid var(--gray-100); - width: 22px; - height: 22px; - background-color: var(--gray-200); + width: 28px; + height: 28px; + font-weight: 300; + color: var(--gray-700); + background-color: var(--gray-100); float: right; cursor: pointer; + margin-top: .25rem; + justify-content: center; } .remove-cart-item-logo { - margin-bottom: 6px; - margin-left: 1px; - } - - .totals { - padding-right: 4rem; - @media (max-width: 992px) { - padding-right: 1rem; - } + margin-top: 2px; + margin-left: 2.2px; + fill: var(--gray-700) !important; } } - .cart-addresses { + .cart-payment-addresses { hr { border-color: var(--border-color); } } + .payment-summary { + h6 { + padding-bottom: 1rem; + border-bottom: solid 1px var(--gray-200); + } + + table { + font-size: 14px; + td { + padding: 0; + padding-top: 0.35rem !important; + border: none !important; + } + + &.grand-total { + border-top: solid 1px var(--gray-200); + } + } + + .bill-label { + color: var(--gray-600); + } + + .bill-content { + font-weight: 500; + &.net-total { + font-size: 16px; + font-weight: 600; + } + } + + .btn-coupon-code { + font-size: 14px; + border: dashed 1px var(--gray-400); + box-shadow: none; + } + } + .number-spinner { width: 75%; min-width: 105px; .cart-btn { border: none; - background: var(--primary-color); - color: white; + background: var(--gray-100); + color: var(--gray-500); box-shadow: none; width: 24px; height: 28px; align-items: center; justify-content: center; display: flex; + font-size: 20px; + font-weight: 300; + color: var(--gray-700); } .cart-qty { height: 28px; font-size: 13px; + &:disabled { + background: var(--gray-100); + opacity: 0.65; + } } } @@ -802,19 +876,29 @@ body.product-page { font-size: 13px; svg use { - stroke: var(--blue-500); + stroke: var(--primary-color); } } .btn-change-address { - color: var(--blue-500); + border: 1px solid var(--primary-color); + color: var(--primary-color); + box-shadow: none; } } +.address-header { + margin-top: .15rem;padding: 0; +} + +.btn-new-address { + float: right; + font-size: 15px !important; + color: var(--primary-color) !important; +} + .btn-new-address:hover, .btn-change-address:hover { - box-shadow: none; - color: var(--blue-500) !important; - border: 1px solid var(--blue-500); + color: var(--primary-color) !important; } .modal .address-card { diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 8b53902d32..31a95896bc 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -649,7 +649,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-23 01:13:54.670763", + "modified": "2021-07-15 12:40:51.074820", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 93de75800d..cf60017373 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -4,18 +4,14 @@ {% set select_address = True %} {% endif %} -{% set show_coupon_code = frappe.db.get_single_value('E Commerce Settings', 'show_apply_coupon_code_in_website') %} -{% if show_coupon_code == 1%} -
-
- - - -
-
-{% endif %}
-
{{ _("Shipping Address") }}
+
+
{{ _("Shipping Address") }}
+ +
+
{% for address in shipping_addresses %} {% if doc.shipping_address_name == address.name %} @@ -27,28 +23,36 @@ {% endif %} {% endfor %}
+ +
-
-
{{ _("Billing Address") }}
-
- {% for address in billing_addresses %} - {% if doc.customer_address == address.name %} -
-
- {% include "templates/includes/cart/address_card.html" %} -
+ +{% if billing_addresses %} +
+
+
{{ _("Billing Address") }}
+
- {% endif %} - {% endfor %} -
- + +
+ {% for address in billing_addresses %} + {% if doc.customer_address == address.name %} +
+
+ {% include "templates/includes/cart/address_card.html" %} +
+
+ {% endif %} + {% endfor %} +
+{% endif %} -
- {% endif %} + {% if doc.terms %} +
+
{{ _("Terms and Conditions") }}
+
+ {{ doc.terms }} +
+
+ {% endif %}
+
diff --git a/erpnext/templates/pages/cart_terms.html b/erpnext/templates/pages/cart_terms.html deleted file mode 100644 index 6d84fb86a7..0000000000 --- a/erpnext/templates/pages/cart_terms.html +++ /dev/null @@ -1,2 +0,0 @@ - -
{{doc.terms}}
diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index b6601a8f67..00aace4f09 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -8,7 +8,7 @@ from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_htm from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website from redisearch import AutoCompleter, Client, Query -from erpnext.e_commerce.website_item_indexing import ( +from erpnext.e_commerce.redisearch import ( is_search_module_loaded, WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE, @@ -18,7 +18,6 @@ from erpnext.e_commerce.website_item_indexing import ( no_cache = 1 - def get_context(context): context.show_search = True @@ -35,13 +34,13 @@ def get_product_data(search=None, start=0, limit=12): # limit = 12 because we show 12 items in the grid view # base query query = """ - Select + SELECT web_item_name, item_name, item_code, brand, route, website_image, thumbnail, item_group, description, web_long_description as website_description, website_warehouse, ranking - from `tabWebsite Item` - where published = 1 + FROM `tabWebsite Item` + WHERE published = 1 """ # search term condition @@ -53,7 +52,7 @@ def get_product_data(search=None, start=0, limit=12): search = "%" + cstr(search) + "%" # order by - query += """ order by ranking asc, modified desc limit %s, %s""" % (cint(start), cint(limit)) + query += """ ORDER BY ranking asc, modified desc limit %s, %s""" % (cint(start), cint(limit)) return frappe.db.sql(query, { "search": search @@ -91,14 +90,9 @@ def search(query, limit=10, fuzzy_search=True): q = Query(query_string) - print(f"Executing query: {q.query_string()}") - results = client.search(q) search_results['results'] = list(map(convert_to_dict, results.docs)) - # FOR DEBUGGING - print("SEARCH RESULTS ------------------\n ", search_results) - return search_results def clean_up_query(query): diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py index a5476e4a62..c66924e3a9 100644 --- a/erpnext/www/all-products/index.py +++ b/erpnext/www/all-products/index.py @@ -1,6 +1,6 @@ import frappe from frappe.utils import cint -from erpnext.e_commerce.filters import ProductFiltersBuilder +from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder sitemap = 1 From 7d1df9d4c3a85fdd716e1767d8693919cf539f9f Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 13 Aug 2021 14:23:52 +0530 Subject: [PATCH 075/225] fix: Error state and passing args for product listing - Show error state in case of unexpected errors in query engine - Pass args appropriately from `view.js` - Use args correctly in `api.py` - Error handling in `api.py` if query engine raises error - Instantiated product data engine tests - Fix dotted path for search api call in `search.js` --- erpnext/e_commerce/api.py | 39 ++++++++++++------- .../test_product_configurator.py | 6 +-- .../test_product_data_engine.py | 38 ++++++++++++++++++ erpnext/e_commerce/product_ui/search.js | 4 +- erpnext/e_commerce/product_ui/views.js | 32 +++++++++------ erpnext/public/scss/shopping_cart.scss | 6 +++ 6 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 erpnext/e_commerce/product_data_engine/test_product_data_engine.py diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py index 4c9f4a79c6..728d3362d3 100644 --- a/erpnext/e_commerce/api.py +++ b/erpnext/e_commerce/api.py @@ -3,6 +3,7 @@ # For license information, please see license.txt import frappe +import json from frappe.utils import cint from erpnext.e_commerce.product_data_engine.query import ProductQuery @@ -10,23 +11,25 @@ from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder from erpnext.setup.doctype.item_group.item_group import get_child_groups @frappe.whitelist(allow_guest=True) -def get_product_filter_data(): - """Get pre-rendered filtered products and discount filters on load.""" - if frappe.form_dict: - search = frappe.form_dict.search - field_filters = frappe.parse_json(frappe.form_dict.field_filters) - attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters) - start = cint(frappe.parse_json(frappe.form_dict.start)) if frappe.form_dict.start else 0 - item_group = frappe.form_dict.item_group - from_filters = frappe.parse_json(frappe.form_dict.from_filters) +def get_product_filter_data(query_args=None): + """Get filtered products and discount filters.""" + if isinstance(query_args, str): + query_args = json.loads(query_args) + + if query_args: + search = query_args.get("search") + field_filters = query_args.get("field_filters", {}) + attribute_filters = query_args.get("attribute_filters", {}) + start = cint(query_args.start) if query_args.get("start") else 0 + item_group = query_args.get("item_group") + from_filters = query_args.get("from_filters") else: search, attribute_filters, item_group, from_filters = None, None, None, None field_filters = {} start = 0 + # if new filter is checked, reset start to show filtered items from page 1 if from_filters: - # if filter is checked, go to start - # and show filtered items from page 1 start = 0 sub_categories = [] @@ -35,8 +38,18 @@ def get_product_filter_data(): sub_categories = get_child_groups(item_group) engine = ProductQuery() - result = engine.query(attribute_filters, field_filters, search_term=search, - start=start, item_group=item_group) + try: + result = engine.query( + attribute_filters, + field_filters, + search_term=search, + start=start, + item_group=item_group + ) + except Exception as e: + traceback = frappe.get_traceback() + frappe.log_error(traceback, frappe._("Product Engine Error")) + return {"exc": "Something went wrong!"} # discount filter data filters = {} diff --git a/erpnext/e_commerce/product_configurator/test_product_configurator.py b/erpnext/e_commerce/product_configurator/test_product_configurator.py index 2f5a24f0b5..fe4ef08995 100644 --- a/erpnext/e_commerce/product_configurator/test_product_configurator.py +++ b/erpnext/e_commerce/product_configurator/test_product_configurator.py @@ -1,13 +1,9 @@ -import unittest - -import frappe -from bs4 import BeautifulSoup import frappe, unittest -from frappe.utils import get_html_for_route from erpnext.e_commerce.product_data_engine.query import ProductQuery from erpnext.e_commerce.doctype.website_item.website_item import make_website_item test_dependencies = ["Item"] +#TODO: Rename to test item variant configurator class TestProductConfigurator(unittest.TestCase): def setUp(self): diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py new file mode 100644 index 0000000000..8bca04634d --- /dev/null +++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py @@ -0,0 +1,38 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + +test_dependencies = ["Item"] + +class TestProductDataEngine(unittest.TestCase): + "Test Products Querying for Product Listing." + def test_product_list_ordering(self): + "Check if website items appear by ranking." + pass + + def test_product_list_paging(self): + pass + + def test_product_list_with_field_filter(self): + pass + + def test_product_list_with_attribute_filter(self): + pass + + def test_product_list_with_discount_filter(self): + pass + + def test_product_list_with_mixed_filtes(self): + pass + + def test_product_list_with_mixed_filtes_item_group(self): + pass + + def test_products_in_multiple_item_groups(self): + "Check if product is visible on multiple item group pages barring its own." + pass + + def test_product_list_with_variants(self): + pass + diff --git a/erpnext/e_commerce/product_ui/search.js b/erpnext/e_commerce/product_ui/search.js index b93c975928..ebe007624a 100644 --- a/erpnext/e_commerce/product_ui/search.js +++ b/erpnext/e_commerce/product_ui/search.js @@ -49,7 +49,7 @@ erpnext.ProductSearch = class { // Fetch and populate product results frappe.call({ - method: "erpnext.templates.pages.e_commerce.product_search.search", + method: "erpnext.templates.pages.product_search.search", args: { query: query }, @@ -61,7 +61,7 @@ erpnext.ProductSearch = class { // Populate categories if (me.category_container) { frappe.call({ - method: "erpnext.templates.pages.e_commerce.product_search.get_category_suggestions", + method: "erpnext.templates.pages.product_search.get_category_suggestions", args: { query: query }, diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js index 0a9ae1f6a5..993fd5c9f0 100644 --- a/erpnext/e_commerce/product_ui/views.js +++ b/erpnext/e_commerce/product_ui/views.js @@ -47,10 +47,14 @@ erpnext.ProductView = class { this.disable_view_toggler(true); frappe.call({ - method: 'erpnext.e_commerce.api.get_product_filter_data', - args: args, + method: "erpnext.e_commerce.api.get_product_filter_data", + args: { + query_args: args + }, callback: function(result) { - if (!result.exc && result && result.message) { + if (!result || result.exc || !result.message || result.message.exc) { + me.render_no_products_section(true); + } else { // Sub Category results are independent of Items if (me.item_group && result.message["sub_categories"].length) { me.render_item_sub_categories(result.message["sub_categories"]); @@ -82,8 +86,6 @@ erpnext.ProductView = class { // Bottom paging me.add_paging_section(result.message["settings"]); - } else { - me.render_no_products_section(); } me.disable_view_toggler(false); @@ -189,7 +191,7 @@ erpnext.ProductView = class { prepare_search() { $(".toolbar").append(` -
+
`); + $(".toolbar").append(`
`); ["btn-list-view", "btn-grid-view"].forEach(view => { let icon = view === "btn-list-view" ? "list" : "image-view"; @@ -473,16 +475,22 @@ erpnext.ProductView = class { } } - render_no_products_section() { - this.products_section.append(` -


-
+ render_no_products_section(error=false) { + let error_section = ` +
+ Something went wrong. Please refresh or contact us. +
+ `; + let no_results_section = ` +
Empty Cart
${ __('No products found') }

- `); + `; + + this.products_section.append(error ? error_section : no_results_section); } render_item_sub_categories(categories) { diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 4de16ca2f8..c719d505a1 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -1316,6 +1316,12 @@ body.product-page { font-size: 14px; } +.alert-error { + color: #e27a84; + background-color: #fff6f7; + border-color: #f5c6cb; +} + .font-md { font-size: 14px !important; } From 80fbe16be8499ba5ef5185f219bf5a013ec06f98 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 17 Aug 2021 00:48:36 +0530 Subject: [PATCH 076/225] test: Product Query & Filter Engine, Item Group Page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Test for ProductQuery engine and ProductFilters engine - Test for engine for Item Group too - Renamed ‘product_configurator’ to ‘variant_selector’ - Cleaned up filters.py - Modal freeze backdrop lighter only in cart, since there’s nothing over it - Fixed unusual spacing in variant selector dialog - Made `get_child_groups_for_website` more readable - Replaced ‘Configure’ with ‘Select’ for variant selection --- erpnext/controllers/item_variant.py | 2 +- erpnext/e_commerce/api.py | 19 +- .../doctype/website_item/test_website_item.py | 55 ++- .../test_product_configurator.py | 91 ----- .../e_commerce/product_data_engine/filters.py | 81 +++-- .../test_item_group_product_data_engine.py | 116 ++++++ .../test_product_data_engine.py | 338 +++++++++++++++++- .../__init__.py | 0 .../item_variants_cache.py | 0 .../variant_selector/test_variant_selector.py | 10 + .../utils.py | 3 +- erpnext/public/scss/shopping_cart.scss | 16 +- .../setup/doctype/item_group/item_group.py | 25 +- erpnext/stock/doctype/item/item.py | 2 +- .../generators/item/item_configure.html | 4 +- .../generators/item/item_configure.js | 10 +- 16 files changed, 574 insertions(+), 198 deletions(-) delete mode 100644 erpnext/e_commerce/product_configurator/test_product_configurator.py create mode 100644 erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py rename erpnext/e_commerce/{product_configurator => variant_selector}/__init__.py (100%) rename erpnext/e_commerce/{product_configurator => variant_selector}/item_variants_cache.py (100%) create mode 100644 erpnext/e_commerce/variant_selector/test_variant_selector.py rename erpnext/e_commerce/{product_configurator => variant_selector}/utils.py (98%) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index f8109ab5a5..68ad702b97 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -132,7 +132,7 @@ def find_variant(template, args, variant_item_code=None): conditions = " or ".join(conditions) - from erpnext.e_commerce.product_configurator.utils import get_item_codes_by_attributes + from erpnext.e_commerce.variant_selector.utils import get_item_codes_by_attributes possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code] for variant in possible_variants: diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py index 728d3362d3..01bde2984c 100644 --- a/erpnext/e_commerce/api.py +++ b/erpnext/e_commerce/api.py @@ -8,11 +8,22 @@ from frappe.utils import cint from erpnext.e_commerce.product_data_engine.query import ProductQuery from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder -from erpnext.setup.doctype.item_group.item_group import get_child_groups +from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website @frappe.whitelist(allow_guest=True) def get_product_filter_data(query_args=None): - """Get filtered products and discount filters.""" + """ + Returns filtered products and discount filters. + :param query_args (dict): contains filters to get products list + + Query Args filters: + search (str): Search Term. + field_filters (dict): Keys include item_group, brand, etc. + attribute_filters(dict): Keys include Color, Size, etc. + start (int): Offset items by + item_group (str): Valid Item Group + from_filters (bool): Set as True to jump to page 1 + """ if isinstance(query_args, str): query_args = json.loads(query_args) @@ -35,7 +46,7 @@ def get_product_filter_data(query_args=None): sub_categories = [] if item_group: field_filters['item_group'] = item_group - sub_categories = get_child_groups(item_group) + sub_categories = get_child_groups_for_website(item_group, immediate=True) engine = ProductQuery() try: @@ -46,7 +57,7 @@ def get_product_filter_data(query_args=None): start=start, item_group=item_group ) - except Exception as e: + except Exception: traceback = frappe.get_traceback() frappe.log_error(traceback, frappe._("Product Engine Error")) return {"exc": "Something went wrong!"} diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index 9afca251b4..4a8e820028 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -26,6 +26,10 @@ class TestWebsiteItem(unittest.TestCase): "price_list": "_Test Price List India" }) + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + def setUp(self): if self._testMethodName in WEBITEM_DESK_TESTS: make_item("Test Web Item", { @@ -38,22 +42,13 @@ class TestWebsiteItem(unittest.TestCase): ] }) elif self._testMethodName in WEBITEM_PRICE_TESTS: - self.create_regular_web_item() + create_regular_web_item() make_web_item_price(item_code="Test Mobile Phone") make_web_pricing_rule( title="Test Pricing Rule for Test Mobile Phone", item_code="Test Mobile Phone", selling=1) - def tearDown(self): - if self._testMethodName in WEBITEM_DESK_TESTS: - frappe.get_doc("Item", "Test Web Item").delete() - elif self._testMethodName in WEBITEM_PRICE_TESTS: - frappe.delete_doc("Pricing Rule", "Test Pricing Rule for Test Mobile Phone") - frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone"}).delete() - frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete() - - def test_index_creation(self): "Check if index is getting created in db." from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update @@ -105,6 +100,8 @@ class TestWebsiteItem(unittest.TestCase): item.reload() self.assertEqual(item.published_in_website, 0) + item.delete() + def test_publish_variant_and_template(self): "Check if template is published on publishing variant." # template "Test Web Item" created on setUp @@ -256,7 +253,7 @@ class TestWebsiteItem(unittest.TestCase): 2) Showing stock availability disabled """ item_code = "Test Mobile Phone" - self.create_regular_web_item() + create_regular_web_item() setup_e_commerce_settings({"show_stock_availability": 1}) frappe.local.shopping_cart_settings = None @@ -298,7 +295,7 @@ class TestWebsiteItem(unittest.TestCase): from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry item_code = "Test Mobile Phone" - self.create_regular_web_item() + create_regular_web_item() setup_e_commerce_settings({"show_stock_availability": 1}) frappe.local.shopping_cart_settings = None @@ -339,7 +336,7 @@ class TestWebsiteItem(unittest.TestCase): def test_recommended_item(self): "Check if added recommended items are fetched correctly." item_code = "Test Mobile Phone" - web_item = self.create_regular_web_item(item_code) + web_item = create_regular_web_item(item_code) setup_e_commerce_settings({ "enable_recommendations": 1, @@ -347,7 +344,7 @@ class TestWebsiteItem(unittest.TestCase): }) # create recommended web item and price for it - recommended_web_item = self.create_regular_web_item("Test Mobile Phone 1") + recommended_web_item = create_regular_web_item("Test Mobile Phone 1") make_web_item_price(item_code="Test Mobile Phone 1") # add recommended item to first web item @@ -379,14 +376,14 @@ class TestWebsiteItem(unittest.TestCase): self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched # tear down - frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone 1"}).delete() web_item.delete() recommended_web_item.delete() + frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete() def test_recommended_item_for_guest_user(self): "Check if added recommended items are fetched correctly for guest user." item_code = "Test Mobile Phone" - web_item = self.create_regular_web_item(item_code) + web_item = create_regular_web_item(item_code) # price visible to guests setup_e_commerce_settings({ @@ -396,7 +393,7 @@ class TestWebsiteItem(unittest.TestCase): }) # create recommended web item and price for it - recommended_web_item = self.create_regular_web_item("Test Mobile Phone 1") + recommended_web_item = create_regular_web_item("Test Mobile Phone 1") make_web_item_price(item_code="Test Mobile Phone 1") # add recommended item to first web item @@ -428,22 +425,24 @@ class TestWebsiteItem(unittest.TestCase): # tear down frappe.set_user("Administrator") - frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone 1"}).delete() web_item.delete() recommended_web_item.delete() + frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete() - def create_regular_web_item(self, item_code=None): - "Create Regular Item and Website Item." - item_code = item_code or "Test Mobile Phone" - item = make_item(item_code) +def create_regular_web_item(item_code=None, item_args=None, web_args=None): + "Create Regular Item and Website Item." + item_code = item_code or "Test Mobile Phone" + item = make_item(item_code, properties=item_args) - if not frappe.db.exists("Website Item", {"item_code": item_code}): - web_item = make_website_item(item, save=False) - web_item.save() - else: - web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code}) + if not frappe.db.exists("Website Item", {"item_code": item_code}): + web_item = make_website_item(item, save=False) + if web_args: + web_item.update(web_args) + web_item.save() + else: + web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code}) - return web_item + return web_item def make_web_item_price(**kwargs): item_code = kwargs.get("item_code") diff --git a/erpnext/e_commerce/product_configurator/test_product_configurator.py b/erpnext/e_commerce/product_configurator/test_product_configurator.py deleted file mode 100644 index fe4ef08995..0000000000 --- a/erpnext/e_commerce/product_configurator/test_product_configurator.py +++ /dev/null @@ -1,91 +0,0 @@ -import frappe, unittest -from erpnext.e_commerce.product_data_engine.query import ProductQuery -from erpnext.e_commerce.doctype.website_item.website_item import make_website_item - -test_dependencies = ["Item"] -#TODO: Rename to test item variant configurator - -class TestProductConfigurator(unittest.TestCase): - def setUp(self): - self.create_variant_item() - self.publish_items_on_website() - - # TODO: E-commerce server side tests - # def test_product_list(self): - # template_items = frappe.get_all('Item', {'show_in_website': 1}) - # variant_items = frappe.get_all('Item', {'show_variant_in_website': 1}) - - # products_settings = frappe.get_doc('Products Settings') - # products_settings.enable_field_filters = 1 - # products_settings.append('filter_fields', {'fieldname': 'item_group'}) - # products_settings.append('filter_fields', {'fieldname': 'stock_uom'}) - # products_settings.save() - - # html = get_html_for_route('all-products') - - # soup = BeautifulSoup(html, 'html.parser') - # products_list = soup.find(class_='products-list') - # items = products_list.find_all(class_='card') - # self.assertEqual(len(items), len(template_items + variant_items)) - - # items_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_in_website': 1}) - # variants_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_variant_in_website': 1}) - - # # mock query params - # frappe.form_dict = frappe._dict({ - # 'field_filters': '{"item_group":["_Test Item Group Desktops"]}' - # }) - # html = get_html_for_route('all-products') - # soup = BeautifulSoup(html, 'html.parser') - # products_list = soup.find(class_='products-list') - # items = products_list.find_all(class_='card') - # self.assertEqual(len(items), len(items_with_item_group + variants_with_item_group)) - - - # def test_get_products_for_website(self): - # items = get_products_for_website(attribute_filters={ - # 'Test Size': ['2XL'] - # }) - # self.assertEqual(len(items), 1) - - # def test_products_in_multiple_item_groups(self): - # """Check if product is visible on multiple item group pages barring its own.""" - # from erpnext.shopping_cart.product_query import ProductQuery - - # if not frappe.db.exists("Item Group", {"name": "Tech Items"}): - # item_group_doc = frappe.get_doc({ - # "doctype": "Item Group", - # "item_group_name": "Tech Items", - # "parent_item_group": "All Item Groups", - # "show_in_website": 1 - # }).insert() - # else: - # item_group_doc = frappe.get_doc("Item Group", "Tech Items") - - # doc = self.create_regular_web_item("Portal Item", item_group="Tech Items") - # if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}): - # doc.append("website_item_groups", { - # "item_group": "_Test Item Group Desktops" - # }) - # doc.save() - - # # check if item is visible in its own Item Group's page - # engine = ProductQuery() - # result = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items") - # items = result["items"] - - # self.assertEqual(len(items), 1) - # self.assertEqual(items[0].item_code, "Portal Item") - - # # check if item is visible in configured foreign Item Group's page - # engine = ProductQuery() - # result = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops") - # items = result["items"] - # item_codes = [row.item_code for row in items] - - # self.assertIn(len(items), [2, 3]) - # self.assertIn("Portal Item", item_codes) - - # # teardown - # doc.delete() - # item_group_doc.delete() diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py index 75137a7ea0..7ae87eba86 100644 --- a/erpnext/e_commerce/product_data_engine/filters.py +++ b/erpnext/e_commerce/product_data_engine/filters.py @@ -6,7 +6,7 @@ from frappe.utils import floor class ProductFiltersBuilder: def __init__(self, item_group=None): - if not item_group or item_group == "E Commerce Settings": + if not item_group: self.doc = frappe.get_doc("E Commerce Settings") else: self.doc = frappe.get_doc("Item Group", item_group) @@ -17,36 +17,39 @@ class ProductFiltersBuilder: if not self.item_group and not self.doc.enable_field_filters: return - filter_fields = [row.fieldname for row in self.doc.filter_fields] + fields, filter_data = [], [] + filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings - meta = frappe.get_meta('Item') - fields = [df for df in meta.fields if df.fieldname in filter_fields] + # filter valid field filters i.e. those that exist in Item + item_meta = frappe.get_meta('Item', cached=True) + fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] - filter_data = [] for df in fields: - filters, or_filters = {}, [] + item_filters, item_or_filters = {}, [] + link_doctype_values = self.get_filtered_link_doctype_records(df) + if df.fieldtype == "Link": if self.item_group: - or_filters.extend([ + item_or_filters.extend([ ["item_group", "=", self.item_group], - ["Website Item Group", "item_group", "=", self.item_group] + ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname) + # Get link field values attached to published items + item_filters['published_in_website'] = 1 + item_values = frappe.get_all( + "Item", + fields=[df.fieldname], + filters=item_filters, + or_filters=item_or_filters, + distinct="True", + pluck=df.fieldname + ) + + values = list(set(item_values) & link_doctype_values) # intersection of both else: - doctype = df.get_link_doctype() - - # apply enable/disable/show_in_website filter - meta = frappe.get_meta(doctype) - - if meta.has_field('enabled'): - filters['enabled'] = 1 - if meta.has_field('disabled'): - filters['disabled'] = 0 - if meta.has_field('show_in_website'): - filters['show_in_website'] = 1 - - values = [d.name for d in frappe.get_all(doctype, filters)] + # table multiselect + values = list(link_doctype_values) # Remove None if None in values: @@ -57,6 +60,36 @@ class ProductFiltersBuilder: return filter_data + def get_filtered_link_doctype_records(self, field): + """ + Get valid link doctype records depending on filters. + Apply enable/disable/show_in_website filter. + Returns: + set: A set containing valid record names + """ + link_doctype = field.get_link_doctype() + meta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None + if meta: + filters = self.get_link_doctype_filters(meta) + link_doctype_values = set(d.name for d in frappe.get_all(link_doctype, filters)) + + return link_doctype_values if meta else set() + + def get_link_doctype_filters(self, meta): + "Filters for Link Doctype eg. 'show_in_website'." + filters = {} + if not meta: + return filters + + if meta.has_field('enabled'): + filters['enabled'] = 1 + if meta.has_field('disabled'): + filters['disabled'] = 0 + if meta.has_field('show_in_website'): + filters['show_in_website'] = 1 + + return filters + def get_attribute_filters(self): if not self.item_group and not self.doc.enable_attribute_filters: return @@ -92,9 +125,9 @@ class ProductFiltersBuilder: def get_discount_filters(self, discounts): discount_filters = [] - # [25.89, 60.5] + # [25.89, 60.5] min max min_discount, max_discount = discounts[0], discounts[1] - # [25, 60] + # [25, 60] rounded min max 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 diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py new file mode 100644 index 0000000000..264fbd8bf7 --- /dev/null +++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py @@ -0,0 +1,116 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +import unittest + +from erpnext.e_commerce.api import get_product_filter_data +from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item + +test_dependencies = ["Item", "Item Group"] + +class TestItemGroupProductDataEngine(unittest.TestCase): + "Test Products & Sub-Category Querying for Product Listing on Item Group Page." + + @classmethod + def setUpClass(cls): + item_codes = [ + ("Test Mobile A", "_Test Item Group B"), + ("Test Mobile B", "_Test Item Group B"), + ("Test Mobile C", "_Test Item Group B - 1"), + ("Test Mobile D", "_Test Item Group B - 1"), + ("Test Mobile E", "_Test Item Group B - 2") + ] + for item in item_codes: + item_code = item[0] + item_args = {"item_group": item[1]} + if not frappe.db.exists("Website Item", {"item_code": item_code}): + create_regular_web_item(item_code, item_args=item_args) + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + def test_product_listing_in_item_group(self): + "Test if only products belonging to the Item Group are fetched." + result = get_product_filter_data(query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B" + }) + + items = result.get("items") + item_codes = [item.get("item_code") for item in items] + + self.assertEqual(len(items), 2) + self.assertIn("Test Mobile A", item_codes) + self.assertNotIn("Test Mobile C", item_codes) + + def test_products_in_multiple_item_groups(self): + """Test if product is visible on multiple item group pages barring its own.""" + website_item = frappe.get_doc("Website Item", {"item_code": "Test Mobile E"}) + + # show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well + website_item.append("website_item_groups", { + "item_group": "_Test Item Group B - 1" + }) + website_item.save() + + result = get_product_filter_data(query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B - 1" + }) + + items = result.get("items") + item_codes = [item.get("item_code") for item in items] + + self.assertEqual(len(items), 3) + self.assertIn("Test Mobile E", item_codes) # visible in other item groups + self.assertIn("Test Mobile C", item_codes) + self.assertIn("Test Mobile D", item_codes) + + result = get_product_filter_data(query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B - 2" + }) + + items = result.get("items") + + self.assertEqual(len(items), 1) + self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group + + def test_item_group_with_sub_groups(self): + "Test Valid Sub Item Groups in Item Group Page." + frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) + frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0) + + result = get_product_filter_data(query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B" + }) + + self.assertTrue(bool(result.get("sub_categories"))) + + child_groups = [d.name for d in result.get("sub_categories")] + # check if child group is fetched if shown in website + self.assertIn("_Test Item Group B - 1", child_groups) + + frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1) + result = get_product_filter_data(query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B" + }) + child_groups = [d.name for d in result.get("sub_categories")] + + # check if child group is fetched if shown in website + self.assertIn("_Test Item Group B - 1", child_groups) + self.assertIn("_Test Item Group B - 2", child_groups) \ No newline at end of file diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py index 8bca04634d..9a7cb3c3cb 100644 --- a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py +++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py @@ -2,37 +2,337 @@ # For license information, please see license.txt import frappe +import unittest -test_dependencies = ["Item"] +from erpnext.e_commerce.product_data_engine.query import ProductQuery +from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder +from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item +from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import setup_e_commerce_settings + +test_dependencies = ["Item", "Item Group"] class TestProductDataEngine(unittest.TestCase): - "Test Products Querying for Product Listing." - def test_product_list_ordering(self): - "Check if website items appear by ranking." - pass + "Test Products Querying and Filters for Product Listing." - def test_product_list_paging(self): - pass + @classmethod + def setUpClass(cls): + item_codes = [ + ("Test 11I Laptop", "Products"), # rank 1 + ("Test 12I Laptop", "Products"), # rank 2 + ("Test 13I Laptop", "Products"), # rank 3 + ("Test 14I Laptop", "Raw Material"), # rank 4 + ("Test 15I Laptop", "Raw Material"), # rank 5 + ("Test 16I Laptop", "Raw Material"), # rank 6 + ("Test 17I Laptop", "Products") # rank 7 + ] + for index, item in enumerate(item_codes, start=1): + item_code = item[0] + item_args = {"item_group": item[1]} + web_args = {"ranking": index} + if not frappe.db.exists("Website Item", {"item_code": item_code}): + create_regular_web_item(item_code, item_args=item_args, web_args=web_args) + + setup_e_commerce_settings({ + "products_per_page": 4, + "enable_field_filters": 1, + "filter_fields": [{"fieldname": "item_group"}], + "enable_attribute_filters": 1, + "filter_attributes": [{"attribute": "Test Size"}] + }) + frappe.local.shopping_cart_settings = None + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + def test_product_list_ordering_and_paging(self): + "Test if website items appear by ranking on different pages." + engine = ProductQuery() + result = engine.query( + attributes={}, + fields={}, + search_term=None, + start=0, + item_group=None + ) + items = result.get("items") + + self.assertIsNotNone(items) + self.assertEqual(len(items), 4) + self.assertGreater(result.get("items_count"), 4) + + # check if items appear as per ranking set in setUpClass + self.assertEqual(items[0].get("item_code"), "Test 17I Laptop") + self.assertEqual(items[1].get("item_code"), "Test 16I Laptop") + self.assertEqual(items[2].get("item_code"), "Test 15I Laptop") + self.assertEqual(items[3].get("item_code"), "Test 14I Laptop") + + # check next page + result = engine.query( + attributes={}, + fields={}, + search_term=None, + start=4, + item_group=None + ) + items = result.get("items") + + # check if items appear as per ranking set in setUpClass on next page + self.assertEqual(items[0].get("item_code"), "Test 13I Laptop") + self.assertEqual(items[1].get("item_code"), "Test 12I Laptop") + self.assertEqual(items[2].get("item_code"), "Test 11I Laptop") + + def test_change_product_ranking(self): + "Test if item on second page appear on first if ranking is changed." + item_code = "Test 12I Laptop" + old_ranking = frappe.db.get_value("Website Item", {"item_code": item_code}, "ranking") + + # low rank, appears on second page + self.assertEqual(old_ranking, 2) + + # set ranking as highest rank + frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", 10) + + engine = ProductQuery() + result = engine.query( + attributes={}, + fields={}, + search_term=None, + start=0, + item_group=None + ) + items = result.get("items") + + # check if item is the first item on the first page + self.assertEqual(items[0].get("item_code"), item_code) + self.assertEqual(items[1].get("item_code"), "Test 17I Laptop") + + # tear down + frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", old_ranking) + + def test_product_list_field_filter_builder(self): + "Test if field filters are fetched correctly." + frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 0) + + filter_engine = ProductFiltersBuilder() + field_filters = filter_engine.get_field_filters() + + # Web Items belonging to 'Products' and 'Raw Material' are available + # but only 'Products' has 'show_in_website' enabled + item_group_filters = field_filters[0] + docfield = item_group_filters[0] + valid_item_groups = item_group_filters[1] + + self.assertEqual(docfield.options, "Item Group") + self.assertIn("Products", valid_item_groups) + self.assertNotIn("Raw Material", valid_item_groups) + + frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 1) + field_filters = filter_engine.get_field_filters() + + #'Products' and 'Raw Materials' both have 'show_in_website' enabled + item_group_filters = field_filters[0] + docfield = item_group_filters[0] + valid_item_groups = item_group_filters[1] + + self.assertEqual(docfield.options, "Item Group") + self.assertIn("Products", valid_item_groups) + self.assertIn("Raw Material", valid_item_groups) def test_product_list_with_field_filter(self): - pass + "Test if field filters are applied correctly." + field_filters = {"item_group": "Raw Material"} + + engine = ProductQuery() + result = engine.query( + attributes={}, + fields=field_filters, + search_term=None, + start=0, + item_group=None + ) + items = result.get("items") + + # check if only 'Raw Material' are fetched in the right order + self.assertEqual(len(items), 3) + self.assertEqual(items[0].get("item_code"), "Test 16I Laptop") + self.assertEqual(items[1].get("item_code"), "Test 15I Laptop") + + # def test_product_list_with_field_filter_table_multiselect(self): + # TODO + # pass + + def test_product_list_attribute_filter_builder(self): + "Test if attribute filters are fetched correctly." + create_variant_web_item() + + filter_engine = ProductFiltersBuilder() + attribute_filter = filter_engine.get_attribute_filters()[0] + attribute = attribute_filter.item_attribute_values[0] + + self.assertEqual(attribute_filter.name, "Test Size") + self.assertEqual(len(attribute_filter.item_attribute_values), 1) + self.assertEqual(attribute.attribute_value, "Large") def test_product_list_with_attribute_filter(self): - pass + "Test if attribute filters are applied correctly." + create_variant_web_item() - def test_product_list_with_discount_filter(self): - pass + attribute_filters = {"Test Size": ["Large"]} + engine = ProductQuery() + result = engine.query( + attributes=attribute_filters, + fields={}, + search_term=None, + start=0, + item_group=None + ) + items = result.get("items") - def test_product_list_with_mixed_filtes(self): - pass + # check if only items with Test Size 'Large' are fetched + self.assertEqual(len(items), 1) + self.assertEqual(items[0].get("item_code"), "Test Web Item-L") - def test_product_list_with_mixed_filtes_item_group(self): - pass + def test_product_list_discount_filter_builder(self): + "Test if discount filters are fetched correctly." + from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price, make_web_pricing_rule - def test_products_in_multiple_item_groups(self): - "Check if product is visible on multiple item group pages barring its own." - pass + item_code = "Test 12I Laptop" + make_web_item_price(item_code=item_code) + make_web_pricing_rule( + title=f"Test Pricing Rule for {item_code}", + item_code=item_code, + selling=1 + ) + + setup_e_commerce_settings({"show_price": 1}) + frappe.local.shopping_cart_settings = None + + engine = ProductQuery() + result = engine.query( + attributes={}, + fields={}, + search_term=None, + start=4, + item_group=None + ) + + self.assertTrue(bool(result.get("discounts"))) + + filter_engine = ProductFiltersBuilder() + discount_filters = filter_engine.get_discount_filters(result["discounts"]) + + self.assertEqual(len(discount_filters[0]), 2) + self.assertEqual(discount_filters[0][0], 10) + self.assertEqual(discount_filters[0][1], "10% and above") + + def test_product_list_with_discount_filters(self): + "Test if discount filters are applied correctly." + from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price, make_web_pricing_rule + + field_filters = {"discount": [10]} + + make_web_item_price(item_code="Test 12I Laptop") + make_web_pricing_rule( + title="Test Pricing Rule for Test 12I Laptop", # 10% discount + item_code="Test 12I Laptop", + selling=1 + ) + make_web_item_price(item_code="Test 13I Laptop") + make_web_pricing_rule( + title="Test Pricing Rule for Test 13I Laptop", # 15% discount + item_code="Test 13I Laptop", + discount_percentage=15, + selling=1 + ) + + setup_e_commerce_settings({"show_price": 1}) + frappe.local.shopping_cart_settings = None + + engine = ProductQuery() + result = engine.query( + attributes={}, + fields=field_filters, + search_term=None, + start=0, + item_group=None + ) + items = result.get("items") + + # check if only product with 10% and above discount are fetched in the right order + self.assertEqual(len(items), 2) + self.assertEqual(items[0].get("item_code"), "Test 13I Laptop") + self.assertEqual(items[1].get("item_code"), "Test 12I Laptop") + + def test_product_list_with_api(self): + "Test products listing using API." + from erpnext.e_commerce.api import get_product_filter_data + + create_variant_web_item() + + result = get_product_filter_data(query_args={ + "field_filters": { + "item_group": "Products" + }, + "attribute_filters": { + "Test Size": ["Large"] + }, + "start": 0 + }) + + items = result.get("items") + + self.assertEqual(len(items), 1) + self.assertEqual(items[0].get("item_code"), "Test Web Item-L") def test_product_list_with_variants(self): - pass + "Test if variants are hideen on hiding variants in settings." + create_variant_web_item() + setup_e_commerce_settings({ + "enable_attribute_filters": 0, + "hide_variants": 1 + }) + frappe.local.shopping_cart_settings = None + + attribute_filters = {"Test Size": ["Large"]} + engine = ProductQuery() + result = engine.query( + attributes=attribute_filters, + fields={}, + search_term=None, + start=0, + item_group=None + ) + items = result.get("items") + + # check if any variants are fetched even though published variant exists + self.assertEqual(len(items), 0) + + # tear down + setup_e_commerce_settings({ + "enable_attribute_filters": 1, + "hide_variants": 0 + }) + +def create_variant_web_item(): + "Create Variant and Template Website Items." + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.controllers.item_variant import create_variant + from erpnext.e_commerce.doctype.website_item.website_item import make_website_item + + make_item("Test Web Item", { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [ + { + "attribute": "Test Size" + } + ] + }) + if not frappe.db.exists("Item", "Test Web Item-L"): + variant = create_variant("Test Web Item", {"Test Size": "Large"}) + variant.save() + + if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}): + make_website_item(variant, save=True) \ No newline at end of file diff --git a/erpnext/e_commerce/product_configurator/__init__.py b/erpnext/e_commerce/variant_selector/__init__.py similarity index 100% rename from erpnext/e_commerce/product_configurator/__init__.py rename to erpnext/e_commerce/variant_selector/__init__.py diff --git a/erpnext/e_commerce/product_configurator/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py similarity index 100% rename from erpnext/e_commerce/product_configurator/item_variants_cache.py rename to erpnext/e_commerce/variant_selector/item_variants_cache.py diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py new file mode 100644 index 0000000000..3eeca173fa --- /dev/null +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -0,0 +1,10 @@ +# import frappe +import unittest +# from erpnext.e_commerce.product_data_engine.query import ProductQuery +# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item + +test_dependencies = ["Item"] + +class TestVariantSelector(unittest.TestCase): + # TODO: Variant Selector Tests + pass \ No newline at end of file diff --git a/erpnext/e_commerce/product_configurator/utils.py b/erpnext/e_commerce/variant_selector/utils.py similarity index 98% rename from erpnext/e_commerce/product_configurator/utils.py rename to erpnext/e_commerce/variant_selector/utils.py index 5ea32f9f9f..2e1852c025 100644 --- a/erpnext/e_commerce/product_configurator/utils.py +++ b/erpnext/e_commerce/variant_selector/utils.py @@ -1,7 +1,6 @@ import frappe from frappe.utils import cint - -from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager +from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager def get_item_codes_by_attributes(attribute_filters, template_item_code=None): items = [] diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index c719d505a1..bfca3f4bf1 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -502,12 +502,7 @@ body.product-page { } .item-configurator-dialog { - .modal-header { - padding: var(--padding-md) var(--padding-xl); - } - .modal-body { - padding: 0 var(--padding-xl); padding-bottom: var(--padding-xl); .status-area { @@ -1292,13 +1287,10 @@ body.product-page { font-size: 72px; } -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - left: 0; - background-color: var(--gray-100); - height: 100%; +[data-path="cart"] { + .modal-backdrop { + background-color: var(--gray-50); // lighter backdrop only on cart freeze + } } .item-thumb { diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index f4b667ecc5..3d1a1d0418 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -104,16 +104,23 @@ class ItemGroup(NestedSet, WebsiteGenerator): def delete_child_item_groups_key(self): frappe.cache().hdel("child_item_groups", self.name) - def validate_item_group_defaults(self): - from erpnext.stock.doctype.item.item import validate_item_default_company_links - validate_item_default_company_links(self.item_group_defaults) - -def get_child_groups(item_group_name): +def get_child_groups_for_website(item_group_name, immediate=False): """Returns child item groups *excluding* passed group.""" - item_group = frappe.get_doc("Item Group", item_group_name) - 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) + item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) + filters = { + "lft": [">", item_group.lft], + "rgt": ["<", item_group.rgt], + "show_in_website": 1 + } + + if immediate: + filters["parent_item_group"] = item_group_name + + return frappe.get_all( + "Item Group", + filters=filters, + fields=["name", "route"] + ) def get_child_item_groups(item_group_name): item_group = frappe.get_cached_value("Item Group", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index de023cad15..52e3b40590 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -914,7 +914,7 @@ def invalidate_cache_for_item(doc): def invalidate_item_variants_cache_for_website(doc): """Rebuild ItemVariantsCacheManager via Item or Website Item.""" - from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager + from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager item_code = None is_web_item = doc.get("published_in_website") or doc.get("published") diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html index 9ff1d79e6d..fcab594402 100644 --- a/erpnext/templates/generators/item/item_configure.html +++ b/erpnext/templates/generators/item/item_configure.html @@ -3,11 +3,11 @@
{% if cart_settings.enable_variants | int %} - {% endif %} {% if cart_settings.show_contact_us_button %} diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index f47650a27e..3220226f7f 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -29,7 +29,7 @@ class ItemConfigure { }); this.dialog = new frappe.ui.Dialog({ - title: __('Configure {0}', [this.item_name]), + title: __('Select Variant for {0}', [this.item_name]), fields, on_hide: () => { set_continue_configuration(); @@ -280,14 +280,14 @@ class ItemConfigure { } get_next_attribute_and_values(selected_attributes) { - return this.call('erpnext.e_commerce.product_configurator.utils.get_next_attribute_and_values', { + return this.call('erpnext.e_commerce.variant_selector.utils.get_next_attribute_and_values', { item_code: this.item_code, selected_attributes }); } get_attributes_and_values() { - return this.call('erpnext.e_commerce.product_configurator.utils.get_attributes_and_values', { + return this.call('erpnext.e_commerce.variant_selector.utils.get_attributes_and_values', { item_code: this.item_code }); } @@ -311,9 +311,9 @@ function set_continue_configuration() { const { itemCode } = $btn_configure.data(); if (localStorage.getItem(`configure:${itemCode}`)) { - $btn_configure.text(__('Continue Configuration')); + $btn_configure.text(__('Continue Selection')); } else { - $btn_configure.text(__('Configure')); + $btn_configure.text(__('Select Variant')); } } From 9aeb2111427a00eb3130cdf7226c92bb581e39e5 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Aug 2021 13:52:46 +0530 Subject: [PATCH 077/225] chore: Remove `Home Page is Products` and Sider fixes - Removed `Home Page is Products` checkbox in E Comm Settings. Can be manually set in Website Settings - Removed hooks trigger to reset home page as products - Sider: duplicate color attribute, shift `return` to next line, over-indentation --- .../e_commerce_settings.json | 15 +-------------- .../e_commerce_settings.py | 11 ----------- erpnext/hooks.py | 3 --- .../v13_0/populate_e_commerce_settings.py | 2 +- erpnext/public/scss/shopping_cart.scss | 1 - erpnext/stock/doctype/item/item.py | 19 ++++++++++--------- 6 files changed, 12 insertions(+), 39 deletions(-) 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 54e88ca081..abc1c4eab0 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 @@ -5,8 +5,6 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "home_page_is_products", - "column_break_4", "products_per_page", "display_settings_section", "hide_variants", @@ -59,13 +57,6 @@ "is_redisearch_loaded" ], "fields": [ - { - "default": "0", - "description": "If checked, the Home page will be the default Item Group for the website", - "fieldname": "home_page_is_products", - "fieldtype": "Check", - "label": "Home Page is Products" - }, { "default": "6", "fieldname": "products_per_page", @@ -84,10 +75,6 @@ "fieldtype": "Check", "label": "Hide Variants" }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, { "default": "0", "description": "The field filters will also work as categories in the Shop by Category page.", @@ -392,7 +379,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-07-15 16:50:50.087281", + "modified": "2021-08-24 13:40:15.294696", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index 1b5987376f..1e54146658 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -17,11 +17,6 @@ class ECommerceSettings(Document): self.is_redisearch_loaded = is_search_module_loaded() def validate(self): - if self.home_page_is_products: - frappe.db.set_value("Website Settings", None, "home_page", "products") - elif frappe.db.get_single_value("Website Settings", "home_page") == 'products': - frappe.db.set_value("Website Settings", None, "home_page", "home") - self.validate_field_filters() self.validate_attribute_filters() self.validate_checkout() @@ -163,9 +158,3 @@ def check_shopping_cart_enabled(): def show_attachments(): return get_shopping_cart_settings().show_attachments - -def home_page_is_products(doc, method): - """Called on saving Website Settings.""" - home_page_is_products = cint(frappe.db.get_single_value("E Commerce Settings", "home_page_is_products")) - if home_page_is_products: - doc.home_page = "products" \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index b364b2f845..3eb6ce7a12 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -241,9 +241,6 @@ doc_events = { ("Sales Taxes and Charges Template", "Price List"): { "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings" }, - "Website Settings": { - "validate": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.home_page_is_products" - }, "Tax Category": { "validate": "erpnext.regional.india.utils.validate_tax_category" }, diff --git a/erpnext/patches/v13_0/populate_e_commerce_settings.py b/erpnext/patches/v13_0/populate_e_commerce_settings.py index 5ce7acb8bb..1565ac2033 100644 --- a/erpnext/patches/v13_0/populate_e_commerce_settings.py +++ b/erpnext/patches/v13_0/populate_e_commerce_settings.py @@ -8,7 +8,7 @@ def execute(): frappe.reload_doc("portal", "doctype", "website_attribute") products_settings_fields = [ - "hide_variants", "home_page_is_products", "products_per_page", + "hide_variants", "products_per_page", "enable_attribute_filters", "enable_field_filters" ] diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index bfca3f4bf1..7753378d7b 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -811,7 +811,6 @@ body.product-page { .cart-btn { border: none; background: var(--gray-100); - color: var(--gray-500); box-shadow: none; width: 24px; height: 28px; diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 52e3b40590..5b8b96e1c3 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -254,7 +254,8 @@ class Item(Document): else: changed[field] = self.get(field) - if not changed: return + if not changed: + return web_item_doc = frappe.get_doc("Website Item", web_item) web_item_doc.update(changed) @@ -462,16 +463,16 @@ class Item(Document): def validate_properties_before_merge(self, new_name): # Validate properties before merging - if not frappe.db.exists("Item", new_name): - frappe.throw(_("Item {0} does not exist").format(new_name)) + if not frappe.db.exists("Item", new_name): + frappe.throw(_("Item {0} does not exist").format(new_name)) - field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"] - new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)] + field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"] + new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)] - if new_properties != [cstr(self.get(field)) for field in field_list]: - msg = _("To merge, following properties must be same for both items") - msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) - frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + if new_properties != [cstr(self.get(field)) for field in field_list]: + msg = _("To merge, following properties must be same for both items") + msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) + frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) def validate_duplicate_website_item_before_merge(self, old_name, new_name): """ From 6b2b9dcee2576362d18aa7055b79c71d5326e021 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 25 Aug 2021 13:09:35 +0530 Subject: [PATCH 078/225] refactor: Cache Item Reviews and other review feedback - `get_doc` -> `get_values` and `db.sql` -> `db.delete` in Wishlist Item deletion - cache first page of Item Reviews and burst cache on addition and deletion of reviews - Update redisearch docs link in E Commerce Settings - Removed unused cint import - Broke setting attribute context into smaller functions and code cleanup - Minor recommended items padding tweak - Item reviews form dict now uses website item as key - Customer reviews rendered from UI style consistency - Stock status consistency in listing and full page - Handle no price in variant dialog for matched item --- .../e_commerce_settings.json | 4 +- .../e_commerce_settings.py | 2 +- .../doctype/item_review/item_review.py | 99 ++++++++++---- .../doctype/website_item/website_item.py | 129 ++++++++++-------- .../e_commerce/doctype/wishlist/wishlist.py | 23 ++-- .../e_commerce/product_data_engine/query.py | 23 +++- .../e_commerce/shopping_cart/product_info.py | 2 +- erpnext/public/scss/shopping_cart.scss | 19 +-- .../generators/item/item_add_to_cart.html | 4 +- .../generators/item/item_configure.js | 2 +- .../generators/item/item_reviews.html | 2 +- erpnext/templates/pages/customer_reviews.js | 26 ++-- erpnext/templates/pages/customer_reviews.py | 8 +- erpnext/utilities/product.py | 4 +- 14 files changed, 218 insertions(+), 129 deletions(-) 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 abc1c4eab0..fc2a56567b 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 @@ -329,7 +329,7 @@ "fieldname": "redisearch_warning", "fieldtype": "HTML", "label": "Redisearch Warning", - "options": "

Redisearch module not loaded. If you want to use advanced product search features, refer documentation here.

" + "options": "

Redisearch is not loaded. If you want to use the advanced product search feature, refer here.

" }, { "default": "0", @@ -379,7 +379,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-08-24 13:40:15.294696", + "modified": "2021-08-24 21:10:45.669526", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index 1e54146658..cade54d106 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -3,7 +3,7 @@ # For license information, please see license.txt import frappe -from frappe.utils import cint, comma_and +from frappe.utils import comma_and from frappe import _, msgprint from frappe.model.document import Document from frappe.utils import unique diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py index b0d8beba75..3f34c4a124 100644 --- a/erpnext/e_commerce/doctype/item_review/item_review.py +++ b/erpnext/e_commerce/doctype/item_review/item_review.py @@ -15,44 +15,94 @@ class UnverifiedReviewer(frappe.ValidationError): pass class ItemReview(Document): - pass + def after_insert(self): + # regenerate cache on review creation + reviews_dict = get_queried_reviews(self.website_item) + set_reviews_in_cache(self.website_item, reviews_dict) + + def after_delete(self): + # regenerate cache on review deletion + reviews_dict = get_queried_reviews(self.website_item) + set_reviews_in_cache(self.website_item, reviews_dict) + @frappe.whitelist() -def get_item_reviews(web_item, start, end, data=None): +def get_item_reviews(web_item, start=0, end=10, data=None): + "Get Website Item Review Data." + start, end = cint(start), cint(end) + settings = get_shopping_cart_settings() + + # Get cached reviews for first page (start=0) + # avoid cache when page is different + from_cache = not bool(start) + if not data: data = frappe._dict() - settings = get_shopping_cart_settings() - if settings and settings.get("enable_reviews"): - data.reviews = frappe.db.get_all("Item Review", filters={"website_item": web_item}, - fields=["*"], limit_start=cint(start), limit_page_length=cint(end)) + reviews_cache = frappe.cache().hget("item_reviews", web_item) + if from_cache and reviews_cache: + data = reviews_cache + else: + data = get_queried_reviews(web_item, start, end, data) + if from_cache: + set_reviews_in_cache(web_item, data) - rating_data = frappe.db.get_all("Item Review", filters={"website_item": web_item}, - fields=["avg(rating) as average, count(*) as total"])[0] - data.average_rating = flt(rating_data.average, 1) - data.average_whole_rating = flt(data.average_rating, 0) + return data - # get % of reviews per rating - reviews_per_rating = [] - for i in range(1,6): - count = frappe.db.get_all("Item Review", filters={"website_item": web_item, "rating": i}, - fields=["count(*) as count"])[0].count +def get_queried_reviews(web_item, start=0, end=10, data=None): + """ + Query Website Item wise reviews and cache if needed. + Cache stores only first page of reviews i.e. 10 reviews maximum. + Returns: + dict: Containing reviews, average ratings, % of reviews per rating and total reviews. + """ + if not data: + data = frappe._dict() - percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0 - reviews_per_rating.append(percent) + data.reviews = frappe.db.get_all( + "Item Review", + filters={"website_item": web_item}, + fields=["*"], + limit_start=start, + limit_page_length=end + ) - data.reviews_per_rating = reviews_per_rating - data.total_reviews = rating_data.total + rating_data = frappe.db.get_all( + "Item Review", + filters={"website_item": web_item}, + fields=["avg(rating) as average, count(*) as total"] + )[0] - return data + data.average_rating = flt(rating_data.average, 1) + data.average_whole_rating = flt(data.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={"website_item": web_item, "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) + + data.reviews_per_rating = reviews_per_rating + data.total_reviews = rating_data.total + + return data + +def set_reviews_in_cache(web_item, reviews_dict): + frappe.cache().hset("item_reviews", web_item, reviews_dict) @frappe.whitelist() def add_item_review(web_item, title, rating, comment=None): """ Add an Item Review by a user if non-existent. """ if frappe.session.user == "Guest": - frappe.throw(_("You are not verified to write a review yet. Please contact us for verification."), - exc=UnverifiedReviewer) + # guest user should not reach here ideally in the case they do via an API, throw error + frappe.throw(_("You are not verified to write a review yet."), exc=UnverifiedReviewer) if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}): doc = frappe.get_doc({ @@ -88,5 +138,6 @@ def get_customer(silent=False): elif silent: return None else: - frappe.throw(_("You are not verified to write a review yet. Please contact us for verification."), - exc=UnverifiedReviewer) \ No newline at end of file + # should not reach here unless via an API + frappe.throw(_("You are not a verified customer yet. Please contact us to proceed."), + exc=UnverifiedReviewer) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 4216126940..4f3f220e09 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -204,7 +204,9 @@ class WebsiteItem(WebsiteGenerator): self.get_product_details_section(context) if settings.enable_reviews: - get_item_reviews(self.name, 0, 4, context) + reviews_data = get_item_reviews(self.name) + context.update(reviews_data) + context.reviews = context.reviews[:4] context.wished = False if frappe.db.exists("Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}): @@ -219,32 +221,38 @@ class WebsiteItem(WebsiteGenerator): return context def set_variant_context(self, context): - if self.has_variants: - context.no_cache = True + if not self.has_variants: + return - # load variants - # also used in set_attribute_context - context.variants = frappe.get_all( - "Item", - filters={"variant_of": self.item_code, "published_in_website": 1}, - order_by="name asc") + context.no_cache = True + variant = frappe.form_dict.variant - variant = frappe.form_dict.variant - if not variant and context.variants: - # the case when the item is opened for the first time from its list - variant = context.variants[0] + # load variants + # also used in set_attribute_context + context.variants = frappe.get_all( + "Item", + filters={ + "variant_of": self.item_code, + "published_in_website": 1 + }, + order_by="name asc") - if variant: - context.variant = frappe.get_doc("Item", variant) + # the case when the item is opened for the first time from its list + if not variant and context.variants: + variant = context.variants[0] - for fieldname in ("website_image", "website_image_alt", "web_long_description", "description", - "website_specifications"): - if context.variant.get(fieldname): - value = context.variant.get(fieldname) - if isinstance(value, list): - value = [d.as_dict() for d in value] + if variant: + context.variant = frappe.get_doc("Item", variant) + fields = ("website_image", "website_image_alt", "web_long_description", "description", + "website_specifications") - context[fieldname] = value + for fieldname in fields: + if context.variant.get(fieldname): + value = context.variant.get(fieldname) + if isinstance(value, list): + value = [d.as_dict() for d in value] + + context[fieldname] = value if self.slideshow: if context.variant and context.variant.slideshow: @@ -253,48 +261,57 @@ class WebsiteItem(WebsiteGenerator): context.update(get_slideshow(self)) def set_attribute_context(self, context): - if self.has_variants: - attribute_values_available = {} - context.attribute_values = {} - context.selected_attributes = {} + if not self.has_variants: + return - # load attributes - for v in context.variants: - v.attributes = frappe.get_all("Item Variant Attribute", - fields=["attribute", "attribute_value"], - filters={"parent": v.name}) - # make a map for easier access in templates - v.attribute_map = frappe._dict({}) - for attr in v.attributes: - v.attribute_map[attr.attribute] = attr.attribute_value + attribute_values_available = {} + context.attribute_values = {} + context.selected_attributes = {} - for attr in v.attributes: - values = attribute_values_available.setdefault(attr.attribute, []) - if attr.attribute_value not in values: - values.append(attr.attribute_value) + # load attributes + self.set_selected_attributes(context.variants, context, attribute_values_available) - if v.name == context.variant.name: - context.selected_attributes[attr.attribute] = attr.attribute_value + # filter attributes, order based on attribute table + item = frappe.get_cached_doc("Item", self.item_code) + self.set_attribute_values(item.attributes, context, attribute_values_available) - # filter attributes, order based on attribute table - item = frappe.get_cached_doc("Item", self.item_code) - for attr in item.attributes: - values = context.attribute_values.setdefault(attr.attribute, []) + context.variant_info = json.dumps(context.variants) - if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): - for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): - values.append(val) + def set_selected_attributes(self, variants, context, attribute_values_available): + for variant in variants: + variant.attributes = frappe.get_all( + "Item Variant Attribute", + filters={"parent": variant.name}, + fields=["attribute", "attribute_value as value"]) - else: - # get list of values defined (for sequence) - for attr_value in frappe.db.get_all("Item Attribute Value", - fields=["attribute_value"], - filters={"parent": attr.attribute}, order_by="idx asc"): + # make an attribute-value map for easier access in templates + variant.attribute_map = frappe._dict( + { attr.attribute : attr.value for attr in variant.attributes} + ) - if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): - values.append(attr_value.attribute_value) + for attr in variant.attributes: + values = attribute_values_available.setdefault(attr.attribute, []) + if attr.value not in values: + values.append(attr.value) - context.variant_info = json.dumps(context.variants) + if variant.name == context.variant.name: + context.selected_attributes[attr.attribute] = attr.value + + def set_attribute_values(self, attributes, context, attribute_values_available): + for attr in attributes: + values = context.attribute_values.setdefault(attr.attribute, []) + + if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): + for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): + values.append(val) + else: + # get list of values defined (for sequence) + for attr_value in frappe.db.get_all("Item Attribute Value", + fields=["attribute_value"], + filters={"parent": attr.attribute}, order_by="idx asc"): + + if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): + values.append(attr_value.attribute_value) def set_disabled_attributes(self, context): """Disable selection options of attribute combinations that do not result in a variant""" diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py index 40b5ad90e1..276ecae10d 100644 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.py +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py @@ -50,16 +50,19 @@ def add_to_wishlist(item_code): @frappe.whitelist() def remove_from_wishlist(item_code): if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}): - frappe.db.sql(""" - DELETE - FROM `tabWishlist Item` - WHERE - item_code=%(item_code)s - and parent='%(user)s' - """ % {"item_code": frappe.db.escape(item_code), "user": frappe.session.user}) - + frappe.db.delete( + "Wishlist Item", + { + "item_code": item_code, + "parent": frappe.session.user + } + ) frappe.db.commit() - wishlist = frappe.get_doc("Wishlist", frappe.session.user) + wishlist_items = frappe.db.get_values( + "Wishlist Item", + filters={"parent": frappe.session.user} + ) + if hasattr(frappe.local, "cookie_manager"): - frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items))) \ No newline at end of file + frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist_items))) \ No newline at end of file diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index 5c67272b96..0ac90906e5 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -2,9 +2,10 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.utils import flt from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website from erpnext.e_commerce.doctype.item_review.item_review import get_customer -from frappe.utils import flt +from erpnext.utilities.product import get_non_stock_item_status from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website @@ -235,12 +236,22 @@ class ProductQuery: def get_stock_availability(self, item): """Modify item object and add stock details.""" item.in_stock = False + warehouse = item.get("website_warehouse") + is_stock_item = frappe.get_cached_value("Item", item.item_code, "is_stock_item") - 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 = bool(stock_qty) + if not is_stock_item: + if warehouse: + # product bundle case + item.in_stock = get_non_stock_item_status(item.item_code, "website_warehouse") + else: + item.in_stock = True + elif warehouse: + # stock item and has warehouse + actual_qty = frappe.db.get_value( + "Bin", + {"item_code": item.item_code,"warehouse": item.get("website_warehouse")}, + "actual_qty") + item.in_stock = bool(flt(actual_qty)) def get_cart_items(self): customer = get_customer(silent=True) diff --git a/erpnext/e_commerce/shopping_cart/product_info.py b/erpnext/e_commerce/shopping_cart/product_info.py index c66abbaf6f..3ebf6d2f8d 100644 --- a/erpnext/e_commerce/shopping_cart/product_info.py +++ b/erpnext/e_commerce/shopping_cart/product_info.py @@ -24,7 +24,7 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): selling_price_list = cart_quotation.get("selling_price_list") if cart_quotation else _set_price_list(cart_settings, None) - price = [] + price = {} if cart_settings.show_price: is_guest = frappe.session.user == "Guest" # Show Price if logged in. diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 7753378d7b..8648cdbf0b 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -188,13 +188,6 @@ body.product-page { font-weight: 600; } - .out-of-stock { - font-weight: 500; - font-size: 14px; - line-height: 20px; - color: #F47A7A; - } - .item-card { padding: var(--padding-sm); min-width: 300px; @@ -450,6 +443,10 @@ body.product-page { .r-item-image { width: 40%; + .product-image { + padding: 2px 15px; + } + .no-image-r-item { display: flex; justify-content: center; background-color: var(--gray-200); @@ -464,7 +461,6 @@ body.product-page { .r-item-info { font-size: 14px; - padding-left: 8px; padding-right: 0; width: 60%; @@ -1322,6 +1318,13 @@ body.product-page { font-weight: 500; } +.out-of-stock { + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: #F47A7A; +} + .mt-minus-2 { margin-top: -2rem; } diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index d52168e657..399240b435 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -35,8 +35,8 @@ {% if cart_settings.show_stock_availability %}
{% if product_info.in_stock == 0 %} - - {{ _('Not in stock') }} + + {{ _('Out of stock') }} {% elif product_info.in_stock == 1 %} diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index 3220226f7f..10cd416d0b 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -214,7 +214,7 @@ class ItemConfigure { ? `