From 5f8b358fd4746394cbe5c349b0e5bd1280c5af3a Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Tue, 19 Mar 2019 11:48:32 +0530
Subject: [PATCH] Website: Product Configurator and Bootstrap 4 (#15965)
- Refactored Homepage with customisable Hero Section
- New Homepage Section to add content on Homepage as cards or using Custom HTML
- Products page at "/all-products" with customisable filters
- Item Configure dialog to find an Item Variant filtered by attribute values
- Contact Us dialog on Item page
- Customisable Item page content using the Website Content field
---
erpnext/config/website.py | 10 +
erpnext/hooks.py | 3 +-
erpnext/hr/doctype/job_opening/job_opening.py | 23 +
.../templates/job_opening_row.html | 9 +
erpnext/patches.txt | 2 +-
.../add_variant_of_in_item_attribute_table.py | 8 +
.../v12_0/set_default_homepage_type.py | 4 +
erpnext/portal/doctype/homepage/homepage.js | 7 +-
erpnext/portal/doctype/homepage/homepage.json | 277 +++-
erpnext/portal/doctype/homepage/homepage.py | 2 -
.../portal/doctype/homepage/test_homepage.py | 19 +
.../doctype/homepage_section/__init__.py | 0
.../homepage_section/homepage_section.js | 6 +
.../homepage_section/homepage_section.json | 336 +++++
.../homepage_section/homepage_section.py | 12 +
.../homepage_section/test_homepage_section.py | 76 +
.../doctype/homepage_section_card/__init__.py | 0
.../homepage_section_card.json | 203 +++
.../homepage_section_card.py | 9 +
.../products_settings/products_settings.js | 11 +
.../products_settings/products_settings.json | 604 ++++----
.../products_settings/products_settings.py | 21 +
.../doctype/website_attribute/__init__.py | 0
.../website_attribute/website_attribute.json | 76 +
.../website_attribute/website_attribute.py | 9 +
.../doctype/website_filter_field/__init__.py | 0
.../website_filter_field.json | 76 +
.../website_filter_field.py | 9 +
.../portal/product_configurator/__init__.py | 0
.../item_variants_cache.py | 94 ++
.../test_product_configurator.py | 84 ++
erpnext/portal/product_configurator/utils.py | 402 ++++++
erpnext/public/build.json | 2 +-
erpnext/public/js/shopping_cart.js | 10 +-
erpnext/public/js/templates/address_list.html | 4 +-
erpnext/public/js/templates/contact_list.html | 4 +-
erpnext/public/js/website_theme.js | 17 +
erpnext/public/less/products.less | 69 +
erpnext/public/less/website.less | 29 +-
erpnext/public/node_modules | 1 +
erpnext/public/scss/website.scss | 53 +
.../quotation_item/quotation_item.json | 146 +-
.../setup/doctype/item_group/item_group.py | 20 +-
.../operations/default_website.py | 2 +-
erpnext/shopping_cart/cart.py | 55 +-
.../shopping_cart_settings.json | 1238 +++++++++--------
erpnext/shopping_cart/product_info.py | 4 +-
erpnext/stock/doctype/item/item.js | 4 +
erpnext/stock/doctype/item/item.json | 74 +-
erpnext/stock/doctype/item/item.py | 101 +-
erpnext/stock/doctype/item/test_records.json | 3 +-
.../doctype/item_attribute/item_attribute.js | 6 +
.../item_attribute/item_attribute.json | 101 +-
.../item_variant_attribute.json | 57 +-
erpnext/templates/generators/item.html | 143 --
erpnext/templates/generators/item/item.html | 32 +
.../generators/item/item_add_to_cart.html | 67 +
.../generators/item/item_configure.html | 23 +
.../generators/item/item_configure.js | 318 +++++
.../generators/item/item_details.html | 22 +
.../templates/generators/item/item_image.html | 107 ++
.../templates/generators/item/item_inquiry.js | 70 +
.../generators/item/item_specifications.html | 16 +
erpnext/templates/generators/item_group.html | 41 +-
erpnext/templates/includes/address_row.html | 12 +-
erpnext/templates/includes/cart.js | 97 +-
.../templates/includes/cart/address_card.html | 12 +
.../templates/includes/cart/cart_address.html | 161 ++-
.../templates/includes/cart/cart_items.html | 69 +-
.../includes/footer/footer_extension.html | 17 +-
.../includes/footer/footer_powered.html | 3 +-
erpnext/templates/includes/macros.html | 43 +-
.../includes/navbar/navbar_items.html | 12 +-
.../includes/order/order_macros.html | 8 +-
.../templates/includes/order/order_taxes.html | 44 +-
erpnext/templates/includes/product_page.js | 215 ---
erpnext/templates/pages/cart.html | 138 +-
erpnext/templates/pages/help.html | 2 +-
erpnext/templates/pages/home.css | 9 +
erpnext/templates/pages/home.html | 130 +-
erpnext/templates/pages/home.py | 45 +-
.../pages/material_request_info.html | 4 +-
.../pages/non_profit/leave-chapter.html | 2 +-
erpnext/templates/pages/order.html | 40 +-
erpnext/templates/pages/product_search.html | 2 +-
erpnext/templates/pages/projects.html | 4 +-
erpnext/templates/pages/task_info.html | 4 +-
erpnext/www/all-products/__init__.py | 0
erpnext/www/all-products/index.html | 163 +++
erpnext/www/all-products/index.js | 161 +++
erpnext/www/all-products/index.py | 26 +
erpnext/www/all-products/item_row.html | 24 +
erpnext/www/all-products/not_found.html | 1 +
93 files changed, 5057 insertions(+), 1622 deletions(-)
create mode 100644 erpnext/hr/doctype/job_opening/templates/job_opening_row.html
create mode 100644 erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py
create mode 100644 erpnext/patches/v12_0/set_default_homepage_type.py
create mode 100644 erpnext/portal/doctype/homepage/test_homepage.py
create mode 100644 erpnext/portal/doctype/homepage_section/__init__.py
create mode 100644 erpnext/portal/doctype/homepage_section/homepage_section.js
create mode 100644 erpnext/portal/doctype/homepage_section/homepage_section.json
create mode 100644 erpnext/portal/doctype/homepage_section/homepage_section.py
create mode 100644 erpnext/portal/doctype/homepage_section/test_homepage_section.py
create mode 100644 erpnext/portal/doctype/homepage_section_card/__init__.py
create mode 100644 erpnext/portal/doctype/homepage_section_card/homepage_section_card.json
create mode 100644 erpnext/portal/doctype/homepage_section_card/homepage_section_card.py
create mode 100644 erpnext/portal/doctype/website_attribute/__init__.py
create mode 100644 erpnext/portal/doctype/website_attribute/website_attribute.json
create mode 100644 erpnext/portal/doctype/website_attribute/website_attribute.py
create mode 100644 erpnext/portal/doctype/website_filter_field/__init__.py
create mode 100644 erpnext/portal/doctype/website_filter_field/website_filter_field.json
create mode 100644 erpnext/portal/doctype/website_filter_field/website_filter_field.py
create mode 100644 erpnext/portal/product_configurator/__init__.py
create mode 100644 erpnext/portal/product_configurator/item_variants_cache.py
create mode 100644 erpnext/portal/product_configurator/test_product_configurator.py
create mode 100644 erpnext/portal/product_configurator/utils.py
create mode 100644 erpnext/public/js/website_theme.js
create mode 100644 erpnext/public/less/products.less
create mode 120000 erpnext/public/node_modules
create mode 100644 erpnext/public/scss/website.scss
create mode 100644 erpnext/stock/doctype/item_attribute/item_attribute.js
delete mode 100644 erpnext/templates/generators/item.html
create mode 100644 erpnext/templates/generators/item/item.html
create mode 100644 erpnext/templates/generators/item/item_add_to_cart.html
create mode 100644 erpnext/templates/generators/item/item_configure.html
create mode 100644 erpnext/templates/generators/item/item_configure.js
create mode 100644 erpnext/templates/generators/item/item_details.html
create mode 100644 erpnext/templates/generators/item/item_image.html
create mode 100644 erpnext/templates/generators/item/item_inquiry.js
create mode 100644 erpnext/templates/generators/item/item_specifications.html
create mode 100644 erpnext/templates/includes/cart/address_card.html
delete mode 100644 erpnext/templates/includes/product_page.js
create mode 100644 erpnext/templates/pages/home.css
create mode 100644 erpnext/www/all-products/__init__.py
create mode 100644 erpnext/www/all-products/index.html
create mode 100644 erpnext/www/all-products/index.js
create mode 100644 erpnext/www/all-products/index.py
create mode 100644 erpnext/www/all-products/item_row.html
create mode 100644 erpnext/www/all-products/not_found.html
diff --git a/erpnext/config/website.py b/erpnext/config/website.py
index 59e7d404d4..d31b057881 100644
--- a/erpnext/config/website.py
+++ b/erpnext/config/website.py
@@ -11,6 +11,16 @@ def get_data():
"name": "Homepage",
"description": _("Settings for website homepage"),
},
+ {
+ "type": "doctype",
+ "name": "Homepage Section",
+ "description": _("Add cards or custom sections on homepage"),
+ },
+ {
+ "type": "doctype",
+ "name": "Products Settings",
+ "description": _("Settings for website product listing"),
+ },
{
"type": "doctype",
"name": "Shopping Cart Settings",
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a6876ac1f3..d28666c48b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -22,7 +22,8 @@ web_include_css = "assets/css/erpnext-web.css"
doctype_js = {
"Communication": "public/js/communication.js",
- "Event": "public/js/event.js"
+ "Event": "public/js/event.js",
+ "Website Theme": "public/js/website_theme.js"
}
welcome_email = "erpnext.setup.utils.welcome_email"
diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py
index 4fc2ac1ded..00883d75f1 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.py
+++ b/erpnext/hr/doctype/job_opening/job_opening.py
@@ -53,3 +53,26 @@ class JobOpening(WebsiteGenerator):
def get_list_context(context):
context.title = _("Jobs")
context.introduction = _('Current Job Openings')
+ context.get_list = get_job_openings
+
+def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None):
+ fields = ['name', 'status', 'job_title', 'description']
+
+ filters = filters or {}
+ filters.update({
+ 'status': 'Open'
+ })
+
+ if txt:
+ filters.update({
+ 'job_title': ['like', '%{0}%'.format(txt)],
+ 'description': ['like', '%{0}%'.format(txt)]
+ })
+
+ return frappe.get_all(doctype,
+ filters,
+ fields,
+ start=limit_start,
+ page_length=limit_page_length,
+ order_by=order_by
+ )
diff --git a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
new file mode 100644
index 0000000000..5da8cc82a2
--- /dev/null
+++ b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
@@ -0,0 +1,9 @@
+
+
{{ doc.job_title }}
+
{{ doc.description }}
+
+
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b7e673da34..cb1e77c73f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -577,6 +577,7 @@ erpnext.patches.v10_0.item_barcode_childtable_migrate # 16-02-2019
erpnext.patches.v11_0.update_delivery_trip_status
erpnext.patches.v11_0.set_missing_gst_hsn_code
erpnext.patches.v11_0.rename_bom_wo_fields
+erpnext.patches.v12_0.set_default_homepage_type
erpnext.patches.v11_0.rename_additional_salary_component_additional_salary
erpnext.patches.v11_0.renamed_from_to_fields_in_project
erpnext.patches.v11_0.add_permissions_in_gst_settings
@@ -584,5 +585,4 @@ erpnext.patches.v11_1.setup_guardian_role
execute:frappe.delete_doc('DocType', 'Notification Control')
erpnext.patches.v11_0.remove_barcodes_field_from_copy_fields_to_variants
erpnext.patches.v12_0.set_task_status
-erpnext.patches.v10_0.item_barcode_childtable_migrate # 16-02-2019
erpnext.patches.v11_0.make_italian_localization_fields # 01-03-2019
diff --git a/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py b/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py
new file mode 100644
index 0000000000..bc6119067c
--- /dev/null
+++ b/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py
@@ -0,0 +1,8 @@
+import frappe
+
+def execute():
+ frappe.db.sql('''
+ UPDATE `tabItem Variant Attribute` t1
+ INNER JOIN `tabItem` t2 ON t2.name = t1.parent
+ SET t1.variant_of = t2.variant_of
+ ''')
diff --git a/erpnext/patches/v12_0/set_default_homepage_type.py b/erpnext/patches/v12_0/set_default_homepage_type.py
new file mode 100644
index 0000000000..241e4b9b5e
--- /dev/null
+++ b/erpnext/patches/v12_0/set_default_homepage_type.py
@@ -0,0 +1,4 @@
+import frappe
+
+def execute():
+ frappe.db.set_value('Homepage', 'Homepage', 'hero_section_based_on', 'Default')
\ No newline at end of file
diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js
index 0b07814f75..ca34d69576 100644
--- a/erpnext/portal/doctype/homepage/homepage.js
+++ b/erpnext/portal/doctype/homepage/homepage.js
@@ -11,7 +11,12 @@ frappe.ui.form.on('Homepage', {
},
refresh: function(frm) {
-
+ frm.add_custom_button(__('Set Meta Tags'), () => {
+ frappe.utils.set_meta_tag('home');
+ });
+ frm.add_custom_button(__('Customize Homepage Sections'), () => {
+ frappe.set_route('List', 'Homepage Section', 'List');
+ });
},
});
diff --git a/erpnext/portal/doctype/homepage/homepage.json b/erpnext/portal/doctype/homepage/homepage.json
index 81433b1c5d..ad27278dc6 100644
--- a/erpnext/portal/doctype/homepage/homepage.json
+++ b/erpnext/portal/doctype/homepage/homepage.json
@@ -1,5 +1,7 @@
{
"allow_copy": 0,
+ "allow_events_in_timeline": 0,
+ "allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
@@ -10,18 +12,24 @@
"doctype": "DocType",
"document_type": "Setup",
"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,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
- "in_list_view": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
@@ -31,24 +39,63 @@
"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
},
{
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
- "fieldname": "title",
- "fieldtype": "Data",
+ "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,
- "label": "TItle",
+ "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
+ },
+ {
+ "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,
@@ -56,16 +103,88 @@
"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": "",
+ "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
+ },
+ {
+ "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
+ },
+ {
+ "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",
@@ -73,7 +192,9 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
- "in_list_view": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
"label": "Tag Line",
"length": 0,
"no_copy": 0,
@@ -82,16 +203,22 @@
"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
},
{
+ "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",
@@ -99,7 +226,9 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
- "in_list_view": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
@@ -108,23 +237,133 @@
"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
},
{
+ "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
+ },
+ {
+ "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
+ },
+ {
+ "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
+ },
+ {
+ "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,
@@ -133,16 +372,21 @@
"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": "/products",
"fieldname": "products_url",
"fieldtype": "Data",
@@ -150,7 +394,9 @@
"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,
@@ -159,16 +405,21 @@
"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,
"description": "Products to be shown on website homepage",
"fieldname": "products",
"fieldtype": "Table",
@@ -176,7 +427,9 @@
"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,
@@ -186,14 +439,17 @@
"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,
@@ -203,7 +459,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
- "modified": "2016-08-29 01:28:00.961623",
+ "modified": "2019-03-02 23:12:59.676202",
"modified_by": "Administrator",
"module": "Portal",
"name": "Homepage",
@@ -212,7 +468,6 @@
"permissions": [
{
"amend": 0,
- "apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
@@ -232,7 +487,6 @@
},
{
"amend": 0,
- "apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
@@ -254,8 +508,11 @@
"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_seen": 0
+ "track_changes": 1,
+ "track_seen": 0,
+ "track_views": 0
}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py
index f8f73fdcd0..4e4d4774ab 100644
--- a/erpnext/portal/doctype/homepage/homepage.py
+++ b/erpnext/portal/doctype/homepage/homepage.py
@@ -9,8 +9,6 @@ from frappe.website.utils import delete_page_cache
class Homepage(Document):
def validate(self):
- if not self.products:
- self.setup_items()
if not self.description:
self.description = frappe._("This is an example website auto-generated from ERPNext")
delete_page_cache('home')
diff --git a/erpnext/portal/doctype/homepage/test_homepage.py b/erpnext/portal/doctype/homepage/test_homepage.py
new file mode 100644
index 0000000000..b262c4640c
--- /dev/null
+++ b/erpnext/portal/doctype/homepage/test_homepage.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.tests.test_website import set_request
+from frappe.website.render import render
+
+class TestHomepage(unittest.TestCase):
+ def test_homepage_load(self):
+ set_request(method='GET', path='home')
+ response = render()
+
+ self.assertEquals(response.status_code, 200)
+
+ html = frappe.safe_decode(response.get_data())
+ self.assertTrue('My custom html',
+ }).insert()
+
+ set_request(method='GET', path='home')
+ response = render()
+
+ self.assertEquals(response.status_code, 200)
+
+ html = frappe.safe_decode(response.get_data())
+
+ soup = BeautifulSoup(html, 'html.parser')
+ sections = soup.find('main').find_all(class_='custom-section')
+ self.assertEqual(len(sections), 1)
+
+ homepage_section = sections[0]
+ self.assertEqual(homepage_section.text, 'My custom html')
+
+ # cleanup
+ frappe.db.rollback()
diff --git a/erpnext/portal/doctype/homepage_section_card/__init__.py b/erpnext/portal/doctype/homepage_section_card/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/portal/doctype/homepage_section_card/homepage_section_card.json b/erpnext/portal/doctype/homepage_section_card/homepage_section_card.json
new file mode 100644
index 0000000000..9092b268c5
--- /dev/null
+++ b/erpnext/portal/doctype/homepage_section_card/homepage_section_card.json
@@ -0,0 +1,203 @@
+{
+ "allow_copy": 0,
+ "allow_events_in_timeline": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2019-02-10 19:39:02.734686",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "title",
+ "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": "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": 1,
+ "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": "subtitle",
+ "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": "Subtitle",
+ "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": "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": "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
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "content",
+ "fieldtype": "Text",
+ "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": "Content",
+ "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": "route",
+ "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": "Route",
+ "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": 0,
+ "istable": 1,
+ "max_attachments": 0,
+ "modified": "2019-02-10 20:11:41.040716",
+ "modified_by": "Administrator",
+ "module": "Portal",
+ "name": "Homepage Section Card",
+ "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
+}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/homepage_section_card/homepage_section_card.py b/erpnext/portal/doctype/homepage_section_card/homepage_section_card.py
new file mode 100644
index 0000000000..bd17279f99
--- /dev/null
+++ b/erpnext/portal/doctype/homepage_section_card/homepage_section_card.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from frappe.model.document import Document
+
+class HomepageSectionCard(Document):
+ pass
diff --git a/erpnext/portal/doctype/products_settings/products_settings.js b/erpnext/portal/doctype/products_settings/products_settings.js
index 7a57abaf9b..b68b5d7aa8 100644
--- a/erpnext/portal/doctype/products_settings/products_settings.js
+++ b/erpnext/portal/doctype/products_settings/products_settings.js
@@ -3,6 +3,17 @@
frappe.ui.form.on('Products Settings', {
refresh: function(frm) {
+ 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
+ ).map(df => ({ label: df.label, value: df.fieldname }));
+
+ const field = frappe.meta.get_docfield("Website Filter Field", "fieldname", frm.docname);
+ field.fieldtype = 'Select';
+ field.options = valid_fields;
+ frm.fields_dict.filter_fields.grid.refresh();
+ });
}
});
diff --git a/erpnext/portal/doctype/products_settings/products_settings.json b/erpnext/portal/doctype/products_settings/products_settings.json
index 69abae13a4..2cf8431497 100644
--- a/erpnext/portal/doctype/products_settings/products_settings.json
+++ b/erpnext/portal/doctype/products_settings/products_settings.json
@@ -1,255 +1,389 @@
{
- "allow_copy": 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",
+ "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,
+ "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,
+ "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": "products_as_list",
- "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 Products as a List",
- "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,
+ "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": "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,
+ "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,
- "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,
- "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,
+ "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,
- "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,
+ "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": "2018-08-14 17:59:58.473100",
- "modified_by": "Administrator",
- "module": "Portal",
- "name": "Products Settings",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "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,
- "apply_user_permissions": 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,
+ "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
+ ],
+ "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/products_settings.py b/erpnext/portal/doctype/products_settings/products_settings.py
index f17ae9fee9..82afebf2f1 100644
--- a/erpnext/portal/doctype/products_settings/products_settings.py
+++ b/erpnext/portal/doctype/products_settings/products_settings.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint
+from frappe import _
from frappe.model.document import Document
class ProductsSettings(Document):
@@ -14,6 +15,26 @@ class ProductsSettings(Document):
website_settings.home_page = 'products'
website_settings.save()
+ self.validate_field_filters()
+ self.validate_attribute_filters()
+
+ 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']]
+
+ 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))
+
+ def validate_attribute_filters(self):
+ 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
+
+
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'))
diff --git a/erpnext/portal/doctype/website_attribute/__init__.py b/erpnext/portal/doctype/website_attribute/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/portal/doctype/website_attribute/website_attribute.json b/erpnext/portal/doctype/website_attribute/website_attribute.json
new file mode 100644
index 0000000000..2874dc432c
--- /dev/null
+++ b/erpnext/portal/doctype/website_attribute/website_attribute.json
@@ -0,0 +1,76 @@
+{
+ "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",
+ "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
+ }
+ ],
+ "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
+}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/website_attribute/website_attribute.py b/erpnext/portal/doctype/website_attribute/website_attribute.py
new file mode 100644
index 0000000000..b8b667a613
--- /dev/null
+++ b/erpnext/portal/doctype/website_attribute/website_attribute.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from frappe.model.document import Document
+
+class WebsiteAttribute(Document):
+ pass
diff --git a/erpnext/portal/doctype/website_filter_field/__init__.py b/erpnext/portal/doctype/website_filter_field/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/portal/doctype/website_filter_field/website_filter_field.json b/erpnext/portal/doctype/website_filter_field/website_filter_field.json
new file mode 100644
index 0000000000..67c0d0ae73
--- /dev/null
+++ b/erpnext/portal/doctype/website_filter_field/website_filter_field.json
@@ -0,0 +1,76 @@
+{
+ "allow_copy": 0,
+ "allow_events_in_timeline": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2018-12-31 17:06:08.716134",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "fieldname",
+ "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": "Fieldname",
+ "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
+ }
+ ],
+ "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 18:26:11.550380",
+ "modified_by": "Administrator",
+ "module": "Portal",
+ "name": "Website Filter Field",
+ "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
+}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/website_filter_field/website_filter_field.py b/erpnext/portal/doctype/website_filter_field/website_filter_field.py
new file mode 100644
index 0000000000..2aa8a6f98d
--- /dev/null
+++ b/erpnext/portal/doctype/website_filter_field/website_filter_field.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from frappe.model.document import Document
+
+class WebsiteFilterField(Document):
+ pass
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/portal/product_configurator/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/portal/product_configurator/item_variants_cache.py b/erpnext/portal/product_configurator/item_variants_cache.py
new file mode 100644
index 0000000000..cd557b5075
--- /dev/null
+++ b/erpnext/portal/product_configurator/item_variants_cache.py
@@ -0,0 +1,94 @@
+import frappe
+
+class ItemVariantsCacheManager:
+ def __init__(self, item_code):
+ self.item_code = item_code
+
+ def get_item_variants_data(self):
+ val = frappe.cache().hget('item_variants_data', self.item_code)
+
+ if not val:
+ self.build_cache()
+
+ return frappe.cache().hget('item_variants_data', self.item_code)
+
+
+ def get_attribute_value_item_map(self):
+ val = frappe.cache().hget('attribute_value_item_map', self.item_code)
+
+ if not val:
+ self.build_cache()
+
+ return frappe.cache().hget('attribute_value_item_map', self.item_code)
+
+
+ def get_item_attribute_value_map(self):
+ val = frappe.cache().hget('item_attribute_value_map', self.item_code)
+
+ if not val:
+ self.build_cache()
+
+ return frappe.cache().hget('item_attribute_value_map', self.item_code)
+
+
+ def get_optional_attributes(self):
+ val = frappe.cache().hget('optional_attributes', self.item_code)
+
+ if not val:
+ self.build_cache()
+
+ return frappe.cache().hget('optional_attributes', self.item_code)
+
+
+ def build_cache(self):
+ parent_item_code = self.item_code
+
+ attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
+ {'parent': parent_item_code}, ['attribute'], order_by='idx asc')
+ ]
+
+ item_variants_data = frappe.db.get_all('Item Variant Attribute',
+ {'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
+ order_by='parent',
+ as_list=1
+ )
+
+ attribute_value_item_map = frappe._dict({})
+ item_attribute_value_map = frappe._dict({})
+
+ for row in item_variants_data:
+ item_code, attribute, attribute_value = row
+ # (attr, value) => [item1, item2]
+ attribute_value_item_map.setdefault((attribute, attribute_value), []).append(item_code)
+ # item => {attr1: value1, attr2: value2}
+ item_attribute_value_map.setdefault(item_code, {})[attribute] = attribute_value
+
+ optional_attributes = set()
+ for item_code, attr_dict in item_attribute_value_map.items():
+ for attribute in attributes:
+ if attribute not in attr_dict:
+ optional_attributes.add(attribute)
+
+ frappe.cache().hset('attribute_value_item_map', parent_item_code, attribute_value_item_map)
+ frappe.cache().hset('item_attribute_value_map', parent_item_code, item_attribute_value_map)
+ frappe.cache().hset('item_variants_data', parent_item_code, item_variants_data)
+ frappe.cache().hset('optional_attributes', parent_item_code, optional_attributes)
+
+ def clear_cache(self):
+ keys = ['attribute_value_item_map', 'item_attribute_value_map', 'item_variants_data', 'optional_attributes']
+
+ for key in keys:
+ frappe.cache().hdel(key, self.item_code)
+
+
+def build_cache(item_code):
+ frappe.cache().hset('item_cache_build_in_progress', item_code, 1)
+ print('ItemVariantsCacheManager: Building cache for', item_code)
+ i = ItemVariantsCacheManager(item_code)
+ i.build_cache()
+ frappe.cache().hset('item_cache_build_in_progress', item_code, 0)
+
+def enqueue_build_cache(item_code):
+ if frappe.cache().hget('item_cache_build_in_progress', item_code):
+ return
+ frappe.enqueue(build_cache, item_code=item_code, queue='short')
diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py
new file mode 100644
index 0000000000..a534e5f838
--- /dev/null
+++ b/erpnext/portal/product_configurator/test_product_configurator.py
@@ -0,0 +1,84 @@
+from __future__ import unicode_literals
+
+from bs4 import BeautifulSoup
+import frappe, unittest
+from frappe.tests.test_website import set_request, get_html_for_route
+from frappe.website.render import render
+from erpnext.portal.product_configurator.utils import get_products_for_website
+from erpnext.stock.doctype.item.test_item import make_item_variant
+
+test_dependencies = ["Item"]
+
+class TestProductConfigurator(unittest.TestCase):
+ def setUp(self):
+ self.create_variant_item()
+
+ 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': ['Medium']
+ })
+ self.assertEqual(len(items), 1)
+
+
+ def create_variant_item(self):
+ if not frappe.db.exists('Item', '_Test Variant Item 1'):
+ frappe.get_doc({
+ "description": "_Test Variant Item 12",
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "variant_of": "_Test Variant Item",
+ "item_code": "_Test Variant Item 1",
+ "item_group": "_Test Item Group",
+ "item_name": "_Test Variant Item 1",
+ "stock_uom": "_Test UOM",
+ "item_defaults": [{
+ "company": "_Test Company",
+ "default_warehouse": "_Test Warehouse - _TC",
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "buying_cost_center": "_Test Cost Center - _TC",
+ "selling_cost_center": "_Test Cost Center - _TC",
+ "income_account": "Sales - _TC"
+ }],
+ "attributes": [
+ {
+ "attribute": "Test Size",
+ "attribute_value": "Medium"
+ }
+ ],
+ "show_variant_in_website": 1
+ }).insert()
+
+
+ def tearDown(self):
+ frappe.db.rollback()
\ No newline at end of file
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
new file mode 100644
index 0000000000..3594bc4467
--- /dev/null
+++ b/erpnext/portal/product_configurator/utils.py
@@ -0,0 +1,402 @@
+import frappe
+from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
+
+def get_field_filter_data():
+ product_settings = get_product_settings()
+ filter_fields = [row.fieldname for row in product_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 filter
+ meta = frappe.get_meta(doctype)
+ filters = {}
+ if meta.has_field('enabled'):
+ filters['enabled'] = 1
+ if meta.has_field('disabled'):
+ filters['disabled'] = 0
+
+ values = [d.name for d in frappe.get_all(doctype, filters)]
+ filter_data.append([f, values])
+
+ return filter_data
+
+
+def get_attribute_filter_data():
+ product_settings = get_product_settings()
+ attributes = [row.attribute for row in product_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)
+
+ 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 get_item_codes_by_attributes(attribute_filters, template_item_code=None):
+ items = []
+
+ for attribute, values in attribute_filters.items():
+ attribute_values = 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.
+ This will ignore the values upon selection of which there cannot exist one item.
+ '''
+ item_cache = ItemVariantsCacheManager(item_code)
+ item_variants_data = item_cache.get_item_variants_data()
+
+ attributes = get_item_attributes(item_code)
+ attribute_list = [a.attribute for a in attributes]
+
+ valid_options = {}
+ for item_code, attribute, attribute_value in item_variants_data:
+ if attribute in attribute_list:
+ valid_options.setdefault(attribute, set()).add(attribute_value)
+
+ for attr in attributes:
+ attr['values'] = valid_options.get(attr.attribute, [])
+
+ return attributes
+
+
+@frappe.whitelist(allow_guest=True)
+def get_next_attribute_and_values(item_code, selected_attributes):
+ '''Find the count of Items that match the selected attributes.
+ Also, find the attribute values that are not applicable for further searching.
+ If less than equal to 10 items are found, return item_codes of those items.
+ If one item is matched exactly, return item_code of that item.
+ '''
+ selected_attributes = frappe.parse_json(selected_attributes)
+
+ item_cache = ItemVariantsCacheManager(item_code)
+ item_variants_data = item_cache.get_item_variants_data()
+
+ attributes = get_item_attributes(item_code)
+ attribute_list = [a.attribute for a in attributes]
+ filtered_items = get_items_with_selected_attributes(item_code, selected_attributes)
+
+ next_attribute = None
+
+ for attribute in attribute_list:
+ if attribute not in selected_attributes:
+ next_attribute = attribute
+ break
+
+ valid_options_for_attributes = frappe._dict({})
+
+ for a in attribute_list:
+ valid_options_for_attributes[a] = set()
+
+ selected_attribute = selected_attributes.get(a, None)
+ if selected_attribute:
+ # already selected attribute values are valid options
+ valid_options_for_attributes[a].add(selected_attribute)
+
+ for row in item_variants_data:
+ item_code, attribute, attribute_value = row
+ if item_code in filtered_items and attribute not in selected_attributes and attribute in attribute_list:
+ valid_options_for_attributes[attribute].add(attribute_value)
+
+ optional_attributes = item_cache.get_optional_attributes()
+ exact_match = []
+ # search for exact match if all selected attributes are required attributes
+ if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):
+ item_attribute_value_map = item_cache.get_item_attribute_value_map()
+ for item_code, attr_dict in item_attribute_value_map.items():
+ if item_code in filtered_items and set(attr_dict.keys()) == set(selected_attributes.keys()):
+ exact_match.append(item_code)
+
+ filtered_items_count = len(filtered_items)
+
+ # get product info if exact match
+ from erpnext.shopping_cart.product_info import get_product_info_for_website
+ if exact_match:
+ data = get_product_info_for_website(exact_match[0])
+ product_info = data.product_info
+ if not data.cart_settings.show_price:
+ product_info = None
+ else:
+ product_info = None
+
+ return {
+ 'next_attribute': next_attribute,
+ 'valid_options_for_attributes': valid_options_for_attributes,
+ 'filtered_items_count': filtered_items_count,
+ 'filtered_items': filtered_items if filtered_items_count < 10 else [],
+ 'exact_match': exact_match,
+ 'product_info': product_info
+ }
+
+
+def get_items_with_selected_attributes(item_code, selected_attributes):
+ item_cache = ItemVariantsCacheManager(item_code)
+ attribute_value_item_map = item_cache.get_attribute_value_item_map()
+
+ items = []
+ for attribute, value in selected_attributes.items():
+ items.append(set(attribute_value_item_map[(attribute, value)]))
+
+ 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
+ products_settings = get_product_settings()
+ page_length = products_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()]
+
+ show_in_website_condition = ''
+ if products_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:
+ search = '%{}%'.format(search)
+ or_filters = [
+ ['name', 'like', search],
+ ['item_name', 'like', search],
+ ['description', 'like', search],
+ ['item_group', 'like', search]
+ ]
+ search_condition = get_conditions(or_filters, 'or')
+
+ filter_condition = get_conditions(filters, 'and')
+
+ where_conditions = ' and '.join(
+ [condition for condition in [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`.`website_image`, `tabItem`.`image`,
+ `tabItem`.`web_long_description`, `tabItem`.`description`,
+ `tabItem`.`route`
+ 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
+
+ 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):
+ attributes = frappe.db.get_all('Item Variant Attribute',
+ fields=['attribute'],
+ filters={
+ 'parenttype': 'Item',
+ 'parent': item_code
+ },
+ order_by='idx asc'
+ )
+
+ optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes()
+
+ for a in attributes:
+ if a.attribute in optional_attributes:
+ a.optional = True
+
+ 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_product_settings():
+ doc = frappe.get_cached_doc('Products Settings')
+ doc.products_per_page = doc.products_per_page or 20
+ return doc
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index c34eef2508..60e72dad71 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -11,7 +11,7 @@
"public/js/shopping_cart.js"
],
"css/erpnext-web.css": [
- "public/less/website.less"
+ "public/scss/website.scss"
],
"js/marketplace.min.js": [
"public/js/hub/marketplace.js"
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
index 7755141da6..5a0526814f 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -48,6 +48,7 @@ $.extend(shopping_cart, {
args: {
item_code: opts.item_code,
qty: opts.qty,
+ additional_notes: opts.additional_notes !== undefined ? opts.additional_notes : undefined,
with_items: opts.with_items || 0
},
btn: opts.btn,
@@ -94,11 +95,12 @@ $.extend(shopping_cart, {
}
},
- shopping_cart_update: function(item_code, newVal, cart_dropdown) {
+ shopping_cart_update: function({item_code, qty, cart_dropdown, additional_notes}) {
frappe.freeze();
shopping_cart.update_cart({
- item_code: item_code,
- qty: newVal,
+ item_code,
+ qty,
+ additional_notes,
with_items: 1,
btn: this,
callback: function(r) {
@@ -131,7 +133,7 @@ $.extend(shopping_cart, {
}
input.val(newVal);
var item_code = input.attr("data-item-code");
- shopping_cart.shopping_cart_update(item_code, newVal, true);
+ shopping_cart.shopping_cart_update({item_code, qty: newVal, cart_dropdown: true});
return false;
});
diff --git a/erpnext/public/js/templates/address_list.html b/erpnext/public/js/templates/address_list.html
index 2379ef6b48..0bc86edb08 100644
--- a/erpnext/public/js/templates/address_list.html
+++ b/erpnext/public/js/templates/address_list.html
@@ -9,7 +9,7 @@
({%= __("Shipping") %}){% } %}
{%= __("Edit") %}
@@ -19,5 +19,5 @@
{% if(!addr_list.length) { %}
{%= __("No address added yet.") %}
{% } %}
-
+
diff --git a/erpnext/public/js/templates/contact_list.html b/erpnext/public/js/templates/contact_list.html
index 893b4e0ec2..2144893961 100644
--- a/erpnext/public/js/templates/contact_list.html
+++ b/erpnext/public/js/templates/contact_list.html
@@ -10,7 +10,7 @@
– {%= contact_list[i].designation %}
{% } %}
{%= __("Edit") %}
@@ -33,6 +33,6 @@
{% if(!contact_list.length) { %}
{%= __("No contacts added yet.") %}
{% } %}
-