Browse Source

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
develop
Faris Ansari 6 years ago
committed by GitHub
parent
commit
5f8b358fd4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      erpnext/config/website.py
  2. 3
      erpnext/hooks.py
  3. 23
      erpnext/hr/doctype/job_opening/job_opening.py
  4. 9
      erpnext/hr/doctype/job_opening/templates/job_opening_row.html
  5. 2
      erpnext/patches.txt
  6. 8
      erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py
  7. 4
      erpnext/patches/v12_0/set_default_homepage_type.py
  8. 7
      erpnext/portal/doctype/homepage/homepage.js
  9. 273
      erpnext/portal/doctype/homepage/homepage.json
  10. 2
      erpnext/portal/doctype/homepage/homepage.py
  11. 19
      erpnext/portal/doctype/homepage/test_homepage.py
  12. 0
      erpnext/portal/doctype/homepage_section/__init__.py
  13. 6
      erpnext/portal/doctype/homepage_section/homepage_section.js
  14. 336
      erpnext/portal/doctype/homepage_section/homepage_section.json
  15. 12
      erpnext/portal/doctype/homepage_section/homepage_section.py
  16. 76
      erpnext/portal/doctype/homepage_section/test_homepage_section.py
  17. 0
      erpnext/portal/doctype/homepage_section_card/__init__.py
  18. 203
      erpnext/portal/doctype/homepage_section_card/homepage_section_card.json
  19. 9
      erpnext/portal/doctype/homepage_section_card/homepage_section_card.py
  20. 11
      erpnext/portal/doctype/products_settings/products_settings.js
  21. 604
      erpnext/portal/doctype/products_settings/products_settings.json
  22. 21
      erpnext/portal/doctype/products_settings/products_settings.py
  23. 0
      erpnext/portal/doctype/website_attribute/__init__.py
  24. 76
      erpnext/portal/doctype/website_attribute/website_attribute.json
  25. 9
      erpnext/portal/doctype/website_attribute/website_attribute.py
  26. 0
      erpnext/portal/doctype/website_filter_field/__init__.py
  27. 76
      erpnext/portal/doctype/website_filter_field/website_filter_field.json
  28. 9
      erpnext/portal/doctype/website_filter_field/website_filter_field.py
  29. 0
      erpnext/portal/product_configurator/__init__.py
  30. 94
      erpnext/portal/product_configurator/item_variants_cache.py
  31. 84
      erpnext/portal/product_configurator/test_product_configurator.py
  32. 402
      erpnext/portal/product_configurator/utils.py
  33. 2
      erpnext/public/build.json
  34. 10
      erpnext/public/js/shopping_cart.js
  35. 4
      erpnext/public/js/templates/address_list.html
  36. 4
      erpnext/public/js/templates/contact_list.html
  37. 17
      erpnext/public/js/website_theme.js
  38. 69
      erpnext/public/less/products.less
  39. 29
      erpnext/public/less/website.less
  40. 1
      erpnext/public/node_modules
  41. 53
      erpnext/public/scss/website.scss
  42. 146
      erpnext/selling/doctype/quotation_item/quotation_item.json
  43. 20
      erpnext/setup/doctype/item_group/item_group.py
  44. 2
      erpnext/setup/setup_wizard/operations/default_website.py
  45. 55
      erpnext/shopping_cart/cart.py
  46. 1238
      erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
  47. 4
      erpnext/shopping_cart/product_info.py
  48. 4
      erpnext/stock/doctype/item/item.js
  49. 74
      erpnext/stock/doctype/item/item.json
  50. 101
      erpnext/stock/doctype/item/item.py
  51. 3
      erpnext/stock/doctype/item/test_records.json
  52. 6
      erpnext/stock/doctype/item_attribute/item_attribute.js
  53. 101
      erpnext/stock/doctype/item_attribute/item_attribute.json
  54. 57
      erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json
  55. 143
      erpnext/templates/generators/item.html
  56. 32
      erpnext/templates/generators/item/item.html
  57. 67
      erpnext/templates/generators/item/item_add_to_cart.html
  58. 23
      erpnext/templates/generators/item/item_configure.html
  59. 318
      erpnext/templates/generators/item/item_configure.js
  60. 22
      erpnext/templates/generators/item/item_details.html
  61. 107
      erpnext/templates/generators/item/item_image.html
  62. 70
      erpnext/templates/generators/item/item_inquiry.js
  63. 16
      erpnext/templates/generators/item/item_specifications.html
  64. 39
      erpnext/templates/generators/item_group.html
  65. 12
      erpnext/templates/includes/address_row.html
  66. 99
      erpnext/templates/includes/cart.js
  67. 12
      erpnext/templates/includes/cart/address_card.html
  68. 159
      erpnext/templates/includes/cart/cart_address.html
  69. 69
      erpnext/templates/includes/cart/cart_items.html
  70. 17
      erpnext/templates/includes/footer/footer_extension.html
  71. 3
      erpnext/templates/includes/footer/footer_powered.html
  72. 43
      erpnext/templates/includes/macros.html
  73. 12
      erpnext/templates/includes/navbar/navbar_items.html
  74. 8
      erpnext/templates/includes/order/order_macros.html
  75. 44
      erpnext/templates/includes/order/order_taxes.html
  76. 215
      erpnext/templates/includes/product_page.js
  77. 136
      erpnext/templates/pages/cart.html
  78. 2
      erpnext/templates/pages/help.html
  79. 9
      erpnext/templates/pages/home.css
  80. 130
      erpnext/templates/pages/home.html
  81. 43
      erpnext/templates/pages/home.py
  82. 4
      erpnext/templates/pages/material_request_info.html
  83. 2
      erpnext/templates/pages/non_profit/leave-chapter.html
  84. 40
      erpnext/templates/pages/order.html
  85. 2
      erpnext/templates/pages/product_search.html
  86. 4
      erpnext/templates/pages/projects.html
  87. 4
      erpnext/templates/pages/task_info.html
  88. 0
      erpnext/www/all-products/__init__.py
  89. 163
      erpnext/www/all-products/index.html
  90. 161
      erpnext/www/all-products/index.js
  91. 26
      erpnext/www/all-products/index.py
  92. 24
      erpnext/www/all-products/item_row.html
  93. 1
      erpnext/www/all-products/not_found.html

10
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",

3
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"

23
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
)

9
erpnext/hr/doctype/job_opening/templates/job_opening_row.html

@ -0,0 +1,9 @@
<div class="my-5">
<h3>{{ doc.job_title }}</h3>
<p>{{ doc.description }}</p>
<div>
<a class="btn btn-primary"
href="/job_application?new=1&job_title={{ doc.name }}">
{{ _("Apply Now") }}</a>
</div>
</div>

2
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

8
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
''')

4
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')

7
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');
});
},
});

273
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,129 @@
"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": "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
},
{
"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
},
{
"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,
"label": "TItle",
"in_standard_filter": 0,
"label": "Hero Section",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -56,16 +169,22 @@
"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
}

2
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')

19
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('<section class="hero-section' in html)

0
erpnext/portal/doctype/homepage_section/__init__.py

6
erpnext/portal/doctype/homepage_section/homepage_section.js

@ -0,0 +1,6 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Homepage Section', {
});

336
erpnext/portal/doctype/homepage_section/homepage_section.json

@ -0,0 +1,336 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2019-02-10 19:42:35.809238",
"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": "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": "Section Based On",
"length": 0,
"no_copy": 0,
"options": "Cards\nCustom HTML",
"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,
"collapsible_depends_on": "",
"columns": 0,
"depends_on": "eval:doc.section_based_on === 'Cards'",
"fieldname": "section_cards_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": "Section Cards",
"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_cards",
"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": "Section Cards",
"length": 0,
"no_copy": 0,
"options": "Homepage Section Card",
"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,
"collapsible_depends_on": "",
"columns": 0,
"default": "3",
"depends_on": "",
"description": "Number of columns for this section. 3 cards will be shown per row if you select 3 columns.",
"fieldname": "no_of_columns",
"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": "Number of Columns",
"length": 0,
"no_copy": 0,
"options": "1\n2\n3\n4\n6",
"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,
"collapsible_depends_on": "",
"columns": 0,
"depends_on": "eval:doc.section_based_on === 'Custom HTML'",
"fieldname": "custom_html_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": "Custom HTML",
"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": "",
"description": "Use this field to render any custom HTML in the section.",
"fieldname": "section_html",
"fieldtype": "Code",
"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": "Section HTML",
"length": 0,
"no_copy": 0,
"options": "HTML",
"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_7",
"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": "",
"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,
"description": "Order in which sections should appear. 0 is first, 1 is second and so on.",
"fieldname": "section_order",
"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": "Section Order",
"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": 0,
"max_attachments": 0,
"modified": "2019-03-04 23:52:31.290468",
"modified_by": "Administrator",
"module": "Portal",
"name": "Homepage Section",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"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",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

12
erpnext/portal/doctype/homepage_section/homepage_section.py

@ -0,0 +1,12 @@
# -*- 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
from frappe.utils import cint
class HomepageSection(Document):
@property
def column_value(self):
return cint(12 / cint(self.no_of_columns or 3))

76
erpnext/portal/doctype/homepage_section/test_homepage_section.py

@ -0,0 +1,76 @@
# -*- 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 bs4 import BeautifulSoup
from frappe.tests.test_website import set_request
from frappe.website.render import render
class TestHomepageSection(unittest.TestCase):
def test_homepage_section_card(self):
try:
frappe.get_doc({
'doctype': 'Homepage Section',
'name': 'Card Section',
'section_based_on': 'Cards',
'section_cards': [
{'title': 'Card 1', 'subtitle': 'Subtitle 1', 'content': 'This is test card 1', 'route': '/card-1'},
{'title': 'Card 2', 'subtitle': 'Subtitle 2', 'content': 'This is test card 2', 'image': 'test.jpg'},
],
'no_of_columns': 3
}).insert()
except frappe.DuplicateEntryError:
pass
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('section')
self.assertEqual(len(sections), 3)
homepage_section = sections[2]
self.assertEqual(homepage_section.h3.text, 'Card Section')
cards = homepage_section.find_all(class_="card")
self.assertEqual(len(cards), 2)
self.assertEqual(cards[0].h5.text, 'Card 1')
self.assertEqual(cards[0].a['href'], '/card-1')
self.assertEqual(cards[1].p.text, 'Subtitle 2')
self.assertEqual(cards[1].find(class_='website-image-lazy')['data-src'], 'test.jpg')
# cleanup
frappe.db.rollback()
def test_homepage_section_custom_html(self):
frappe.get_doc({
'doctype': 'Homepage Section',
'name': 'Custom HTML Section',
'section_based_on': 'Custom HTML',
'section_html': '<div class="custom-section">My custom html</div>',
}).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()

0
erpnext/portal/doctype/homepage_section_card/__init__.py

203
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
}

9
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

11
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();
});
}
});

604
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
}

21
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 <b>{1}</b> 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'))

0
erpnext/portal/doctype/website_attribute/__init__.py

76
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
}

9
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

0
erpnext/portal/doctype/website_filter_field/__init__.py

76
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
}

9
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

0
erpnext/portal/product_configurator/__init__.py

94
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')

84
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()

402
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

2
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"

10
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;
});

4
erpnext/public/js/templates/address_list.html

@ -9,7 +9,7 @@
<span class="text-muted">({%= __("Shipping") %})</span>{% } %}
<a href="#Form/Address/{%= encodeURIComponent(addr_list[i].name) %}"
class="btn btn-default btn-xs pull-right"
class="btn btn-light btn-xs pull-right"
style="margin-top:-3px; margin-right: -5px;">
{%= __("Edit") %}</a>
</p>
@ -19,5 +19,5 @@
{% if(!addr_list.length) { %}
<p class="text-muted small">{%= __("No address added yet.") %}</p>
{% } %}
<p><button class="btn btn-xs btn-default btn-address">{{ __("New Address") }}</button></p>
<p><button class="btn btn-xs btn-light btn-address">{{ __("New Address") }}</button></p>

4
erpnext/public/js/templates/contact_list.html

@ -10,7 +10,7 @@
<span class="text-muted">&ndash; {%= contact_list[i].designation %}</span>
{% } %}
<a href="#Form/Contact/{%= encodeURIComponent(contact_list[i].name) %}"
class="btn btn-xs btn-default pull-right"
class="btn btn-xs btn-light pull-right"
style="margin-top:-3px; margin-right: -5px;">
{%= __("Edit") %}</a>
</p>
@ -33,6 +33,6 @@
{% if(!contact_list.length) { %}
<p class="text-muted small">{%= __("No contacts added yet.") %}</p>
{% } %}
<p><button class="btn btn-xs btn-default btn-contact">
<p><button class="btn btn-xs btn-light btn-contact">
{{ __("New Contact") }}</button>
</p>

17
erpnext/public/js/website_theme.js

@ -0,0 +1,17 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.form.on('Website Theme', {
apply_custom_theme(frm) {
let custom_theme = frm.doc.custom_theme;
custom_theme = custom_theme.split('\n');
if (
frm.doc.apply_custom_theme
&& custom_theme.length === 2
&& custom_theme[1].includes('frappe/public/scss/website')
) {
frm.set_value('custom_theme',
`$primary: #7575ff;\n@import "frappe/public/scss/website";\n@import "erpnext/public/scss/website";`);
}
}
});

69
erpnext/public/less/products.less

@ -0,0 +1,69 @@
@import "variables.less";
.products-list .product-image {
display: inline-block;
width: 160px;
height: 160px;
object-fit: contain;
margin-right: 1rem;
}
.product-image.no-image {
display: flex;
justify-content: center;
align-items: center;
font-size: 3rem;
color: var(--gray);
background: var(--light);
}
.product-image a {
text-decoration: none;
}
.filter-options {
max-height: 300px;
overflow: auto;
}
.item-slideshow-image {
height: 3rem;
width: 3rem;
object-fit: contain;
padding: 0.5rem;
border: 1px solid @border-color;
border-radius: 4px;
cursor: pointer;
&:hover, &.active {
border-color: var(--primary);
}
}
.address-card {
cursor: pointer;
position: relative;
.check {
display: none;
}
&.active {
border-color: var(--primary);
.check {
display: inline-flex;
}
}
}
.check {
display: inline-flex;
padding: 0.25rem;
background: var(--primary);
color: white;
border-radius: 50%;
font-size: 12px;
width: 24px;
height: 24px;
}

29
erpnext/public/less/website.less

@ -245,10 +245,10 @@
}
}
.number-spinner {
width:100px;
margin-top:5px;
}
// .number-spinner {
// width:100px;
// margin-top:5px;
// }
.cart-btn {
border-color: #ccc;
@ -361,3 +361,24 @@
border-color: @brand-primary;
}
}
.item-slideshow-image {
height: 3rem;
width: 3rem;
object-fit: contain;
padding: 0.5rem;
border: 1px solid @border-color;
border-radius: 4px;
cursor: pointer;
&:hover, &.active {
border-color: @brand-primary;
}
}
.section-products {
.card-img-top {
max-height: 300px;
object-fit: contain;
}
}

1
erpnext/public/node_modules

@ -0,0 +1 @@
/Users/netchampfaris/frappe-bench/apps/erpnext/node_modules

53
erpnext/public/scss/website.scss

@ -0,0 +1,53 @@
@import "frappe/public/scss/variables";
.product-image img {
min-height: 20rem;
max-height: 30rem;
}
.filter-options {
max-height: 300px;
overflow: auto;
}
.item-slideshow-image {
height: 3rem;
width: 3rem;
object-fit: contain;
padding: 0.5rem;
border: 1px solid $border-color;
border-radius: 4px;
cursor: pointer;
&:hover, &.active {
border-color: $primary;
}
}
.address-card {
cursor: pointer;
position: relative;
.check {
display: none;
}
&.active {
border-color: $primary;
.check {
display: inline-flex;
}
}
}
.check {
display: inline-flex;
padding: 0.25rem;
background: $primary;
color: white;
border-radius: 50%;
font-size: 12px;
width: 24px;
height: 24px;
}

146
erpnext/selling/doctype/quotation_item/quotation_item.json

@ -1,18 +1,18 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2013-03-07 11:42:57",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2013-03-07 11:42:57",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
@ -1934,32 +1934,96 @@
"translatable": 0,
"unique": 0,
"width": "150px"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "shopping_cart_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": "Shopping Cart",
"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": "additional_notes",
"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": "Additional Notes",
"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": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"menu_index": 0,
"modified": "2019-02-18 18:57:25.277633",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"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,
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"menu_index": 0,
"modified": "2019-01-09 17:49:41.606821",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"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
}
}

20
erpnext/setup/doctype/item_group/item_group.py

@ -71,8 +71,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
"items": get_product_list_for_group(product_group = self.name, start=start,
limit=context.page_length + 1, search=frappe.form_dict.get("search")),
"parents": get_parent_item_groups(self.parent_item_group),
"title": self.name,
"products_as_list": cint(frappe.db.get_single_value('Website Settings', 'products_as_list'))
"title": self.name
})
if self.slideshow:
@ -119,7 +118,7 @@ def get_product_list_for_group(product_group=None, start=0, limit=10, search=Non
for item in data:
set_product_info_for_website(item)
return [get_item_for_list_in_html(r) for r in data]
return data
def get_child_groups_for_list_in_html(item_group, start, limit, search):
search_filters = None
@ -141,7 +140,7 @@ def get_child_groups_for_list_in_html(item_group, start, limit, search):
limit = limit
)
return [get_item_for_list_in_html(r) for r in data]
return data
def adjust_qty_for_expired_items(data):
adjusted_data = []
@ -172,9 +171,7 @@ def get_item_for_list_in_html(context):
context["show_availability_status"] = cint(frappe.db.get_single_value('Products Settings',
'show_availability_status'))
products_template = 'templates/includes/products_as_grid.html'
if cint(frappe.db.get_single_value('Products Settings', 'products_as_list')):
products_template = 'templates/includes/products_as_list.html'
products_template = 'templates/includes/products_as_list.html'
return frappe.get_template(products_template).render(context)
@ -188,15 +185,20 @@ def get_group_item_count(item_group):
def get_parent_item_groups(item_group_name):
base_parents = [
{"name": frappe._("Home"), "route":"/"},
{"name": frappe._("All Products"), "route":"/all-products"},
]
if not item_group_name:
return [{"name": frappe._("Home"), "route":"/"}]
return base_parents
item_group = frappe.get_doc("Item Group", item_group_name)
parent_groups = frappe.db.sql("""select name, route from `tabItem Group`
where lft <= %s and rgt >= %s
and show_in_website=1
order by lft asc""", (item_group.lft, item_group.rgt), as_dict=True)
return [{"name": frappe._("Home"), "route":"/"}] + parent_groups
return base_parents + parent_groups
def invalidate_cache_for(doc, item_group=None):
if not item_group:

2
erpnext/setup/setup_wizard/operations/default_website.py

@ -45,7 +45,7 @@ class website_maker(object):
website_settings.append("top_bar_items", {
"doctype": "Top Bar Item",
"label": _("Products"),
"url": "/products"
"url": "/all-products"
})
website_settings.save()

55
erpnext/shopping_cart/cart.py

@ -45,7 +45,8 @@ def get_cart_quotation(doc=None):
for address in addresses],
"billing_addresses": [{"name": address.name, "display": address.display}
for address in addresses],
"shipping_rules": get_applicable_shipping_rules(party)
"shipping_rules": get_applicable_shipping_rules(party),
"cart_settings": frappe.get_cached_doc("Shopping Cart Settings")
}
@frappe.whitelist()
@ -83,7 +84,14 @@ def place_order():
return sales_order.name
@frappe.whitelist()
def update_cart(item_code, qty, with_items=False):
def request_for_quotation():
quotation = _get_cart_quotation()
quotation.flags.ignore_permissions = True
quotation.submit()
return quotation.name
@frappe.whitelist()
def update_cart(item_code, qty, additional_notes=None, with_items=False):
quotation = _get_cart_quotation()
empty_card = False
@ -101,10 +109,12 @@ def update_cart(item_code, qty, with_items=False):
quotation.append("items", {
"doctype": "Quotation Item",
"item_code": item_code,
"qty": qty
"qty": qty,
"additional_notes": additional_notes
})
else:
quotation_items[0].qty = qty
quotation_items[0].additional_notes = additional_notes
apply_cart_settings(quotation=quotation)
@ -140,6 +150,45 @@ def get_shopping_cart_menu(context=None):
return frappe.render_template('templates/includes/cart/cart_dropdown.html', context)
@frappe.whitelist()
def add_new_address(doc):
doc = frappe.parse_json(doc)
doc.update({
'doctype': 'Address'
})
address = frappe.get_doc(doc)
address.save(ignore_permissions=True)
return address
@frappe.whitelist(allow_guest=True)
def create_lead_for_item_inquiry(lead, subject, message):
lead = frappe.parse_json(lead)
lead_doc = frappe.new_doc('Lead')
lead_doc.update(lead)
lead_doc.set('lead_owner', '')
try:
lead_doc.save(ignore_permissions=True)
except frappe.exceptions.DuplicateEntryError:
frappe.clear_messages()
lead_doc = frappe.get_doc('Lead', {'email_id': lead['email_id']})
lead_doc.add_comment('Comment', text='''
<div>
<h5>{subject}</h5>
<p>{message}</p>
</div>
'''.format(subject=subject, message=message))
return lead_doc
@frappe.whitelist()
def get_terms_and_conditions(terms_name):
return frappe.db.get_value('Terms and Conditions', terms_name, 'terms')
@frappe.whitelist()
def update_cart_address(address_fieldname, address_name):
quotation = _get_cart_quotation()

1238
erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json

File diff suppressed because it is too large

4
erpnext/shopping_cart/product_info.py

@ -41,10 +41,10 @@ def get_product_info_for_website(item_code):
if item:
product_info["qty"] = item[0].qty
return {
return frappe._dict({
"product_info": product_info,
"cart_settings": cart_settings
}
})
def set_product_info_for_website(item):
"""set product price uom for website"""

4
erpnext/stock/doctype/item/item.js

@ -180,6 +180,10 @@ frappe.ui.form.on("Item", {
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);
}
});

74
erpnext/stock/doctype/item/item.json

@ -1,7 +1,7 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_guest_to_view": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:item_code",
@ -3860,6 +3860,39 @@
"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.show_in_website || doc.show_variant_in_website",
"fieldname": "set_meta_tags",
"fieldtype": "Button",
"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": "Set Meta Tags",
"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,
@ -3994,6 +4027,39 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"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",
"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": "Website 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,
@ -4194,7 +4260,7 @@
"unique": 0
}
],
"has_web_view": 0,
"has_web_view": 1,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-tag",
@ -4206,7 +4272,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 1,
"modified": "2019-02-16 17:43:56.039611",
"modified": "2019-03-08 11:47:59.269724",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@ -4377,4 +4443,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
}

101
erpnext/stock/doctype/item/item.py

@ -9,11 +9,11 @@ import erpnext
import frappe
import copy
from erpnext.controllers.item_variant import (ItemVariantExistsError,
copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes)
copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes)
from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
from frappe import _, msgprint
from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate,
now_datetime, random_string, strip)
now_datetime, random_string, strip)
from frappe.utils.html_utils import clean_html
from frappe.website.doctype.website_slideshow.website_slideshow import \
get_slideshow
@ -40,7 +40,7 @@ class Item(WebsiteGenerator):
website = frappe._dict(
page_title_field="item_name",
condition_field="show_in_website",
template="templates/generators/item.html",
template="templates/generators/item/item.html",
no_cache=1
)
@ -160,7 +160,7 @@ class Item(WebsiteGenerator):
'''Add a new price'''
if not price_list:
price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list')
or frappe.db.get_value('Price List', _('Standard Selling')))
or frappe.db.get_value('Price List', _('Standard Selling')))
if price_list:
item_price = frappe.get_doc({
"doctype": "Item Price",
@ -199,7 +199,7 @@ class Item(WebsiteGenerator):
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 if self.item_name else self.item_code) + '-' + random_string(5))
'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5))
def validate_website_image(self):
"""Validate if the website image is a public file"""
@ -222,7 +222,7 @@ class Item(WebsiteGenerator):
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))
.format(self.website_image, self.name))
self.website_image = None
@ -313,6 +313,8 @@ class Item(WebsiteGenerator):
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
@ -323,8 +325,8 @@ class Item(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:
@ -335,7 +337,7 @@ class Item(WebsiteGenerator):
context.variant = frappe.get_doc("Item", variant)
for fieldname in ("website_image", "web_long_description", "description",
"website_specifications"):
"website_specifications"):
if context.variant.get(fieldname):
value = context.variant.get(fieldname)
if isinstance(value, list):
@ -358,8 +360,12 @@ class Item(WebsiteGenerator):
# load attributes
for v in context.variants:
v.attributes = frappe.get_all("Item Variant Attribute",
fields=["attribute", "attribute_value"],
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, [])
@ -431,6 +437,31 @@ class Item(WebsiteGenerator):
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)
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:
@ -533,7 +564,7 @@ class Item(WebsiteGenerator):
warehouse += [d.get("warehouse")]
else:
frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}")
.format(d.idx, d.warehouse), DuplicateReorderRows)
.format(d.idx, d.warehouse), DuplicateReorderRows)
if d.warehouse_reorder_level and not d.warehouse_reorder_qty:
frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx))
@ -553,7 +584,7 @@ class Item(WebsiteGenerator):
def update_item_price(self):
frappe.db.sql("""update `tabItem Price` set item_name=%s,
item_description=%s, brand=%s where item_code=%s""",
(self.item_name, self.description, self.brand, self.name))
(self.item_name, self.description, self.brand, self.name))
def on_trash(self):
super(Item, self).on_trash()
@ -575,7 +606,7 @@ class Item(WebsiteGenerator):
new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)]
if new_properties != [cstr(self.get(fld)) for fld in field_list]:
frappe.throw(_("To merge, following properties must be same for both items")
+ ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]))
+ ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]))
def after_rename(self, old_name, new_name, merge):
if self.route:
@ -598,7 +629,7 @@ class Item(WebsiteGenerator):
item_wise_tax_detail.pop(old_name)
frappe.db.set_value(dt, d.name, "item_wise_tax_detail",
json.dumps(item_wise_tax_detail), update_modified=False)
json.dumps(item_wise_tax_detail), update_modified=False)
def set_last_purchase_rate(self, new_name):
last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0)
@ -626,7 +657,7 @@ class Item(WebsiteGenerator):
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"]):
{"parent": self.item_group}, ["label", "description"]):
row = self.append("website_specifications")
row.label = label
row.description = desc
@ -700,7 +731,7 @@ class Item(WebsiteGenerator):
def update_variants(self):
if self.flags.dont_update_variants or \
frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'):
frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'):
return
if self.has_variants:
variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name})
@ -751,7 +782,7 @@ class Item(WebsiteGenerator):
template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom")
if template_uom != self.stock_uom:
frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'")
.format(self.stock_uom, template_uom))
.format(self.stock_uom, template_uom))
def validate_uom_conversion_factor(self):
if self.uoms:
@ -783,10 +814,14 @@ class Item(WebsiteGenerator):
variant = get_variant(self.variant_of, args, self.name)
if variant:
frappe.throw(_("Item variant {0} exists with same attributes")
.format(variant), ItemVariantExistsError)
.format(variant), ItemVariantExistsError)
validate_item_variant_attributes(self, args)
# copy variant_of value for each attribute row
for d in self.attributes:
d.variant_of = self.variant_of
def get_timeline_data(doctype, name):
'''returns timeline data based on stock ledger entry'''
@ -866,18 +901,18 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
limit 1""", (item_code, cstr(doc_name)), as_dict=1)
purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date
or "1900-01-01")
or "1900-01-01")
purchase_receipt_date = getdate(last_purchase_receipt and
last_purchase_receipt[0].posting_date or "1900-01-01")
last_purchase_receipt[0].posting_date or "1900-01-01")
if (purchase_order_date > purchase_receipt_date) or \
(last_purchase_order and not last_purchase_receipt):
(last_purchase_order and not last_purchase_receipt):
# use purchase order
last_purchase = last_purchase_order[0]
purchase_date = purchase_order_date
elif (purchase_receipt_date > purchase_order_date) or \
(last_purchase_receipt and not last_purchase_order):
(last_purchase_receipt and not last_purchase_order):
# use purchase receipt
last_purchase = last_purchase_receipt[0]
purchase_date = purchase_receipt_date
@ -907,7 +942,7 @@ 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]))
+ [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)
@ -915,6 +950,22 @@ def invalidate_cache_for_item(doc):
if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group:
invalidate_cache_for(doc, doc.old_item_group)
invalidate_item_variants_cache_for_website(doc)
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'):
item_code = doc.variant_of
if item_code:
item_cache = ItemVariantsCacheManager(item_code)
item_cache.clear_cache()
def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
@ -922,7 +973,7 @@ def check_stock_uom_with_bin(item, stock_uom):
matched = True
ref_uom = frappe.db.get_value("Stock Ledger Entry",
{"item_code": item}, "stock_uom")
{"item_code": item}, "stock_uom")
if ref_uom:
if cstr(ref_uom) != cstr(stock_uom):
@ -931,7 +982,7 @@ def check_stock_uom_with_bin(item, stock_uom):
bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1)
for bin in bin_list:
if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0
or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom):
or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom):
matched = False
break

3
erpnext/stock/doctype/item/test_records.json

@ -309,7 +309,8 @@
"warehouse_reorder_level": 20,
"warehouse_reorder_qty": 20
}
]
],
"show_in_website": 1
},
{
"description": "_Test Item 1",

6
erpnext/stock/doctype/item_attribute/item_attribute.js

@ -0,0 +1,6 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Item Attribute', {
});

101
erpnext/stock/doctype/item_attribute/item_attribute.json

@ -1,212 +1,294 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:attribute_name",
"beta": 0,
"creation": "2014-09-26 03:49:54.899170",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "attribute_name",
"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": "Attribute Name",
"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,
"unique": 0
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "numeric_values",
"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": "Numeric Values",
"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": "numeric_values",
"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,
"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": "from_range",
"fieldtype": "Float",
"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": "From Range",
"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": "increment",
"fieldtype": "Float",
"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": "Increment",
"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_8",
"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,
"depends_on": "",
"fieldname": "to_range",
"fieldtype": "Float",
"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": "To Range",
"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.numeric_values",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "item_attribute_values",
"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 Attribute Values",
"length": 0,
"no_copy": 0,
@ -214,24 +296,29 @@
"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,
"icon": "fa fa-edit",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-11-16 06:29:48.198647",
"modified": "2019-01-01 13:17:46.524806",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Attribute",
@ -240,7 +327,6 @@
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
@ -259,8 +345,13 @@
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

57
erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json

@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
@ -14,6 +15,40 @@
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "variant_of",
"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": "Variant Of",
"length": 0,
"no_copy": 0,
"options": "Item",
"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,
@ -41,10 +76,12 @@
"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,
@ -70,10 +107,12 @@
"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,
@ -102,10 +141,12 @@
"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,
@ -133,10 +174,12 @@
"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,
@ -163,10 +206,12 @@
"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,
@ -194,10 +239,12 @@
"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,
@ -225,10 +272,12 @@
"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,
@ -254,10 +303,12 @@
"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,
@ -285,6 +336,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
@ -299,7 +351,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-12-11 11:26:25.126350",
"modified": "2019-01-03 15:36:59.129006",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Variant Attribute",
@ -313,5 +365,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
"track_seen": 0,
"track_views": 0
}

143
erpnext/templates/generators/item.html

@ -1,143 +0,0 @@
{% extends "templates/web.html" %}
{% block title %} {{ title }} {% endblock %}
{% block breadcrumbs %}
{% include "templates/includes/breadcrumbs.html" %}
{% endblock %}
{% block page_content %}
{% from "erpnext/templates/includes/macros.html" import product_image %}
<div class="item-content">
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
<div class="row">
<div class="row">
{% if slideshow %}
{% set slideshow_items = frappe.get_list(doctype="Website Slideshow Item", fields=["image"], filters={ "parent": doc.slideshow }) %}
<div class="col-md-1">
{%- for slideshow_item in slideshow_items -%}
{% set image_src = slideshow_item['image'] %}
{% if image_src %}
<div class="item-alternative-image border">
<img src="{{ image_src }}" height="50" weight="50" />
</div>
{% endif %}
{% endfor %}
</div>
<div class="col-md-5">
<div class="item-image">
{% set first_image = slideshow_items[0]['image'] %}
{{ product_image(first_image, "product-full-image") }}
</div>
</div>
{% else %}
<div class="col-md-6">
{{ product_image(website_image, "product-full-image") }}
</div>
{% endif %}
<div class="col-sm-6">
<h2 itemprop="name">{{ item_name }}</h2>
<p class="text-muted">
{{ _("Item Code") }}: <span itemprop="productID">{{ variant and variant.name or name }}</span>
</p>
<br>
<div class="item-attribute-selectors">
{% if has_variants and attributes %}
{% for d in attributes %}
{% if attribute_values[d.attribute] -%}
<div class="item-view-attribute {% if (attribute_values[d.attribute] | len)==1 -%} hidden {%- endif %}"
style="margin-bottom: 10px;">
<h6 class="text-muted">{{ _(d.attribute) }}</h6>
<select class="form-control"
style="max-width: 140px"
data-attribute="{{ d.attribute }}">
{% for value in attribute_values[d.attribute] %}
<option value="{{ value }}"
{% if selected_attributes and selected_attributes[d.attribute]==value -%}
selected
{%- elif disabled_attributes and value in disabled_attributes.get(d.attribute, []) -%}
disabled
{%- endif %}>
{{ _(value) }}
</option>
{% endfor %}
</select>
</div>
{%- endif %}
{% endfor %}
{% endif %}
</div>
<br>
<div>
<div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
<h4 class="item-price hide" itemprop="price"></h4>
<div class="item-stock hide" itemprop="availability"></div>
</div>
<div class="item-cart hide">
<div id="item-spinner">
<span style="display: inline-block">
<div class="input-group number-spinner">
<span class="input-group-btn">
<button class="btn btn-default cart-btn" data-dir="dwn">
</button>
</span>
<input class="form-control text-right cart-qty" value="1">
<span class="input-group-btn">
<button class="btn btn-default cart-btn" data-dir="up" style="margin-left:-2px;">
+</button>
</span>
</div>
</span>
</div>
<div id="item-add-to-cart">
<button class="btn btn-primary btn-sm">
{{ _("Add to Cart") }}</button>
</div>
<div id="item-update-cart" style="display: none;">
<a href="/cart" class='btn btn-sm btn-default'>
<i class='octicon octicon-check'></i>
{{ _("View in Cart") }}</a>
</div>
</div>
</div>
</div>
</div>
<div class="row item-website-description margin-top">
<div class="col-md-12">
<div class="h6 text-uppercase">{{ _("Description") }}</div>
<div itemprop="description" class="item-desc">
{{ web_long_description or description or _("No description given") }}
</div>
</div>
</div>
{% if website_specifications -%}
<div class="row item-website-specification margin-top">
<div class="col-md-12">
<div class="h6 text-uppercase">{{ _("Specifications") }}</div>
<table class="table">
{% for d in website_specifications -%}
<tr>
<td class="text-muted" style="width: 30%;">{{ d.label }}</td>
<td>{{ d.description }}</td>
</tr>
{%- endfor %}
</table>
</div>
</div>
{%- endif %}
</div>
</div>
</div>
<script>
{% include "templates/includes/product_page.js" %}
{% if variant_info %}
window.variant_info = {{ variant_info }};
{% else %}
window.variant_info = null;
{% endif %}
</script>
{% endblock %}

32
erpnext/templates/generators/item/item.html

@ -0,0 +1,32 @@
{% extends "templates/web.html" %}
{% block title %} {{ title }} {% endblock %}
{% block breadcrumbs %}
{% include "templates/includes/breadcrumbs.html" %}
{% endblock %}
{% block page_content %}
{% from "erpnext/templates/includes/macros.html" import product_image %}
<div class="item-content">
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
<div class="row mb-5">
{% include "templates/generators/item/item_image.html" %}
{% include "templates/generators/item/item_details.html" %}
</div>
{% include "templates/generators/item/item_specifications.html" %}
{{ doc.website_content or '' }}
</div>
</div>
{% endblock %}
{% block base_scripts %}
<!-- js should be loaded in body! -->
<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/assets/js/frappe-web.min.js"></script>
<script type="text/javascript" src="/assets/js/control.min.js"></script>
<script type="text/javascript" src="/assets/js/dialog.min.js"></script>
<script type="text/javascript" src="/assets/js/bootstrap-4-web.min.js"></script>
{% endblock %}

67
erpnext/templates/generators/item/item_add_to_cart.html

@ -0,0 +1,67 @@
{% if shopping_cart and shopping_cart.cart_settings.enabled %}
{% set cart_settings = shopping_cart.cart_settings %}
{% set product_info = shopping_cart.product_info %}
<div class="item-cart row mt-2" data-variant-item-code="{{ item_code }}">
<div class="col-md-12">
{% if cart_settings.show_price and product_info.price %}
<h4>
{{ product_info.price.formatted_price_sales_uom }}
<small class="text-muted">({{ product_info.price.formatted_price }} / {{ product_info.uom }})</small>
</h4>
{% endif %}
{% if cart_settings.show_stock_availability %}
<div>
{% if product_info.in_stock == 0 %}
<span class="text-danger">
{{ _('Not in stock') }}
</span>
{% elif product_info.in_stock == 1 %}
<span class="text-success">
{{ _('In stock') }}
{% if product_info.show_stock_qty and product_info.stock_qty %}
({{ product_info.stock_qty[0][0] }})
{% endif %}
</span>
{% endif %}
</div>
{% endif %}
<div class="mt-3">
<a href="/cart"
class="btn btn-light btn-view-in-cart {% if not product_info.qty %}hidden{% endif %}"
role="button"
>
{{ _("View in Cart") }}
</a>
<button
data-item-code="{{item_code}}"
class="btn btn-outline-primary btn-add-to-cart {% if product_info.qty %}hidden{% endif %}"
>
{{ _("Add to Cart") }}
</button>
</div>
</div>
</div>
<script>
frappe.ready(() => {
$('.page_content').on('click', '.btn-add-to-cart', (e) => {
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
const item_code = $btn.data('item-code');
erpnext.shopping_cart.update_cart({
item_code,
qty: 1,
callback(r) {
$btn.prop('disabled', false);
if (r.message) {
$('.btn-add-to-cart, .btn-view-in-cart').toggleClass('hidden');
}
}
});
});
});
</script>
{% endif %}

23
erpnext/templates/generators/item/item_configure.html

@ -0,0 +1,23 @@
{% if shopping_cart and shopping_cart.cart_settings.enabled %}
{% set cart_settings = shopping_cart.cart_settings %}
<div class="mt-3">
{% if cart_settings.show_configure_button | int %}
<button class="btn btn-primary btn-configure"
data-item-code="{{ doc.name }}"
data-item-name="{{ doc.item_name }}"
>
{{ _('Configure') }}
</button>
{% endif %}
{% if cart_settings.show_contact_us_button | int %}
<button class="btn btn-link btn-inquiry" data-item-code="{{ doc.name }}">
{{ _('Contact Us') }}
</button>
{% endif %}
</div>
<script>
{% include "templates/generators/item/item_configure.js" %}
{% include "templates/generators/item/item_inquiry.js" %}
</script>
{% endif %}

318
erpnext/templates/generators/item/item_configure.js

@ -0,0 +1,318 @@
class ItemConfigure {
constructor(item_code, item_name) {
this.item_code = item_code;
this.item_name = item_name;
this.get_attributes_and_values()
.then(attribute_data => {
this.attribute_data = attribute_data;
this.show_configure_dialog();
});
}
show_configure_dialog() {
const fields = this.attribute_data.map(a => {
return {
fieldtype: 'Select',
label: a.attribute,
fieldname: a.attribute,
options: a.values.map(v => {
return {
label: v,
value: v
};
}),
change: (e) => {
this.on_attribute_selection(e);
}
};
});
this.dialog = new frappe.ui.Dialog({
title: __('Configure {0}', [this.item_name]),
fields,
on_hide: () => {
set_continue_configuration();
}
});
this.attribute_data.forEach(a => {
const field = this.dialog.get_field(a.attribute);
const $a = $(`<a href>${__("Clear")}</a>`);
$a.on('click', (e) => {
e.preventDefault();
this.dialog.set_value(a.attribute, '');
});
field.$wrapper.find('.help-box').append($a);
});
this.append_status_area();
this.dialog.show();
this.dialog.set_values(JSON.parse(localStorage.getItem(this.get_cache_key())));
$('.btn-configure').prop('disabled', false);
}
on_attribute_selection(e) {
if (e) {
const changed_fieldname = $(e.target).data('fieldname');
this.show_range_input_if_applicable(changed_fieldname);
} else {
this.show_range_input_for_all_fields();
}
const values = this.dialog.get_values();
if (Object.keys(values).length === 0) {
this.clear_status();
localStorage.removeItem(this.get_cache_key());
return;
}
// save state
localStorage.setItem(this.get_cache_key(), JSON.stringify(values));
// show
this.set_loading_status();
this.get_next_attribute_and_values(values)
.then(data => {
const {
valid_options_for_attributes,
} = data;
this.set_item_found_status(data);
for (let attribute in valid_options_for_attributes) {
const valid_options = valid_options_for_attributes[attribute];
const options = this.dialog.get_field(attribute).df.options;
const new_options = options.map(o => {
o.disabled = !valid_options.includes(o.value);
return o;
});
this.dialog.set_df_property(attribute, 'options', new_options);
this.dialog.get_field(attribute).set_options();
}
});
}
show_range_input_for_all_fields() {
this.dialog.fields.forEach(f => {
this.show_range_input_if_applicable(f.fieldname);
});
}
show_range_input_if_applicable(fieldname) {
const changed_field = this.dialog.get_field(fieldname);
const changed_value = changed_field.get_value();
if (changed_value && changed_value.includes(' to ')) {
// possible range input
let numbers = changed_value.split(' to ');
numbers = numbers.map(number => parseFloat(number));
if (!numbers.some(n => isNaN(n))) {
numbers.sort((a, b) => a - b);
if (changed_field.$input_wrapper.find('.range-selector').length) {
return;
}
const parent = $('<div class="range-selector">')
.insertBefore(changed_field.$input_wrapper.find('.help-box'));
const control = frappe.ui.form.make_control({
df: {
fieldtype: 'Int',
label: __('Enter value betweeen {0} and {1}', [numbers[0], numbers[1]]),
change: () => {
const value = control.get_value();
if (value < numbers[0] || value > numbers[1]) {
control.$wrapper.addClass('was-validated');
control.set_description(
__('Value must be between {0} and {1}', [numbers[0], numbers[1]]));
control.$input[0].setCustomValidity('error');
} else {
control.$wrapper.removeClass('was-validated');
control.set_description('');
control.$input[0].setCustomValidity('');
this.update_range_values(fieldname, value);
}
}
},
render_input: true,
parent
});
control.$wrapper.addClass('mt-3');
}
}
}
update_range_values(attribute, range_value) {
this.range_values = this.range_values || {};
this.range_values[attribute] = range_value;
}
show_remaining_optional_attributes() {
// show all attributes if remaining
// unselected attributes are all optional
const unselected_attributes = this.dialog.fields.filter(df => {
const value_selected = this.dialog.get_value(df.fieldname);
return !value_selected;
});
const is_optional_attribute = df => {
const optional_attributes = this.attribute_data
.filter(a => a.optional).map(a => a.attribute);
return optional_attributes.includes(df.fieldname);
};
if (unselected_attributes.every(is_optional_attribute)) {
unselected_attributes.forEach(df => {
this.dialog.fields_dict[df.fieldname].$wrapper.show();
});
}
}
set_loading_status() {
this.dialog.$status_area.html(`
<div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
${__('Loading...')}
</div>
`);
}
set_item_found_status(data) {
const html = this.get_html_for_item_found(data);
this.dialog.$status_area.html(html);
}
clear_status() {
this.dialog.$status_area.empty();
}
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) {
const exact_match_message = __('1 exact match.');
const one_item = exact_match.length === 1 ?
exact_match[0] :
filtered_items_count === 1 ?
filtered_items[0] : '';
const item_add_to_cart = one_item ? `
<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
<div>
<div>${one_item} ${product_info && product_info.price ? '(' + product_info.price.formatted_price_sales_uom + ')' : ''}</div>
</div>
<a href data-action="btn_add_to_cart" data-item-code="${one_item}">
${__('Add to cart')}
</a>
</div>
`: '';
const items_found = filtered_items_count === 1 ?
__('{0} item found.', [filtered_items_count]) :
__('{0} items found.', [filtered_items_count]);
const item_found_status = `
<div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
<span>
${exact_match.length === 1 ? '' : items_found}
${exact_match.length === 1 ? `<span>${exact_match_message}</span>` : ''}
</span>
<a href data-action="btn_clear_values">
${__('Clear values')}
</a>
</div>
`;
return `
${item_add_to_cart}
${item_found_status}
`;
}
btn_add_to_cart(e) {
if (frappe.session.user !== 'Guest') {
localStorage.removeItem(this.get_cache_key());
}
const item_code = $(e.currentTarget).data('item-code');
const additional_notes = Object.keys(this.range_values || {}).map(attribute => {
return `${attribute}: ${this.range_values[attribute]}`;
}).join('\n');
erpnext.shopping_cart.update_cart({
item_code,
additional_notes,
qty: 1
});
this.dialog.hide();
}
btn_clear_values() {
this.dialog.fields_list.forEach(f => {
f.df.options = f.df.options.map(option => {
option.disabled = false;
return option;
});
});
this.dialog.clear();
this.on_attribute_selection();
}
append_status_area() {
this.dialog.$status_area = $('<div class="status-area">');
this.dialog.$wrapper.find('.modal-body').prepend(this.dialog.$status_area);
this.dialog.$wrapper.on('click', '[data-action]', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
const action = $target.data('action');
const method = this[action];
method.call(this, e);
});
this.dialog.$body.css({ maxHeight: '75vh', overflow: 'auto', overflowX: 'hidden' });
}
get_next_attribute_and_values(selected_attributes) {
return this.call('erpnext.portal.product_configurator.utils.get_next_attribute_and_values', {
item_code: this.item_code,
selected_attributes
});
}
get_attributes_and_values() {
return this.call('erpnext.portal.product_configurator.utils.get_attributes_and_values', {
item_code: this.item_code
});
}
get_cache_key() {
return `configure:${this.item_code}`;
}
call(method, args) {
// promisified frappe.call
return new Promise((resolve, reject) => {
frappe.call(method, args)
.then(r => resolve(r.message))
.fail(reject);
});
}
}
function set_continue_configuration() {
const $btn_configure = $('.btn-configure');
const { itemCode } = $btn_configure.data();
if (localStorage.getItem(`configure:${itemCode}`)) {
$btn_configure.text(__('Continue Configuration'));
} else {
$btn_configure.text(__('Configure'));
}
}
frappe.ready(() => {
const $btn_configure = $('.btn-configure');
if (!$btn_configure.length) return;
const { itemCode, itemName } = $btn_configure.data();
set_continue_configuration();
$btn_configure.on('click', () => {
$btn_configure.prop('disabled', true);
new ItemConfigure(itemCode, itemName);
});
});

22
erpnext/templates/generators/item/item_details.html

@ -0,0 +1,22 @@
<div class="col-md-8">
<!-- title -->
<h1 itemprop="name">
{{ item_name }}
</h1>
<p class="text-muted">
<span>{{ _("Item Code") }}:</span>
<span itemprop="productID">{{ doc.name }}</span>
</p>
<!-- description -->
<div itemprop="description">
{{ doc.web_long_description or doc.description or _("No description given") | safe }}
</div>
{% if has_variants %}
<!-- configure template -->
{% include "templates/generators/item/item_configure.html" %}
{% else %}
<!-- add variant to cart -->
{% include "templates/generators/item/item_add_to_cart.html" %}
{% endif %}
</div>

107
erpnext/templates/generators/item/item_image.html

@ -0,0 +1,107 @@
<div class="col-md-4 h-100">
{% if slides %}
{{ product_image(slides[0].image, 'product-image') }}
<div class="item-slideshow">
{% for item in slides %}
<img class="item-slideshow-image mt-2 {% if loop.first %}active{% endif %}"
src="{{ item.image }}" alt="{{ item.heading }}">
{% endfor %}
</div>
<!-- Simple image slideshow -->
<script>
frappe.ready(() => {
$('.page_content').on('click', '.item-slideshow-image', (e) => {
const $img = $(e.currentTarget);
const link = $img.prop('src');
const $product_image = $('.product-image');
$product_image.find('a').prop('href', link);
$product_image.find('img').prop('src', link);
$('.item-slideshow-image').removeClass('active');
$img.addClass('active');
});
})
</script>
{% else %}
{{ product_image(website_image or image or 'no-image.jpg') }}
{% endif %}
<!-- Simple image preview -->
<div class="image-zoom-view" style="display: none;">
<button type="button" class="close" aria-label="Close">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<style>
.website-image {
cursor: pointer;
}
.image-zoom-view {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.8);
z-index: 1080;
}
.image-zoom-view img {
max-height: 100%;
max-width: 100%;
}
.image-zoom-view button {
position: absolute;
right: 3rem;
top: 2rem;
}
.image-zoom-view svg {
color: var(--white);
}
</style>
<script>
frappe.ready(() => {
const $zoom_wrapper = $('.image-zoom-view');
$('.website-image').on('click', (e) => {
e.preventDefault();
const $img = $(e.target);
const src = $img.prop('src');
if (!src) return;
show_preview(src);
});
$zoom_wrapper.on('click', 'button', hide_preview);
$(document).on('keydown', (e) => {
if (e.key === 'Escape') {
hide_preview();
}
});
function show_preview(src) {
$zoom_wrapper.show();
const $img = $(`<img src="${src}">`)
$zoom_wrapper.append($img);
}
function hide_preview() {
$zoom_wrapper.find('img').remove();
$zoom_wrapper.hide();
}
})
</script>

70
erpnext/templates/generators/item/item_inquiry.js

@ -0,0 +1,70 @@
frappe.ready(() => {
const d = new frappe.ui.Dialog({
title: __('Contact Us'),
fields: [
{
fieldtype: 'Data',
label: __('Full Name'),
fieldname: 'lead_name',
reqd: 1
},
{
fieldtype: 'Data',
label: __('Organization Name'),
fieldname: 'company_name',
},
{
fieldtype: 'Data',
label: __('Email'),
fieldname: 'email_id',
options: 'Email',
reqd: 1
},
{
fieldtype: 'Data',
label: __('Subject'),
fieldname: 'subject',
reqd: 1
},
{
fieldtype: 'Text',
label: __('Message'),
fieldname: 'message',
reqd: 1
}
],
primary_action: send_inquiry,
primary_action_label: __('Send')
});
function send_inquiry() {
const values = d.get_values();
const doc = Object.assign({}, values);
delete doc.subject;
delete doc.message;
d.hide();
frappe.call('erpnext.shopping_cart.cart.create_lead_for_item_inquiry', {
lead: doc,
subject: values.subject,
message: values.message
}).then(r => {
if (r.message) {
d.clear();
}
});
}
$('.btn-inquiry').click((e) => {
const $btn = $(e.target);
const item_code = $btn.data('item-code');
d.set_value('subject', 'Inquiry about ' + item_code);
if (!['Administrator', 'Guest'].includes(frappe.session.user)) {
d.set_value('email_id', frappe.session.user);
d.set_value('lead_name', frappe.get_cookie('full_name'));
}
d.show();
});
});

16
erpnext/templates/generators/item/item_specifications.html

@ -0,0 +1,16 @@
{% if doc.website_specifications -%}
<div class="row item-website-specification mt-5">
<div class="col-md-12">
<h6 class="text-uppercase text-muted">{{ _("Specifications") }}</h6>
<table class="table table-bordered">
{% for d in doc.website_specifications -%}
<tr>
<td class="text-muted" style="width: 30%;">{{ d.label }}</td>
<td>{{ d.description }}</td>
</tr>
{%- endfor %}
</table>
</div>
</div>
{%- endif %}

39
erpnext/templates/generators/item_group.html

@ -9,29 +9,32 @@
{% include "templates/includes/slideshow.html" %}
{% endif %}
{% if description %}<!-- description -->
<div itemprop="description">{{ description or ""}}</div>
<div class="mb-3" itemprop="description">{{ description or ""}}</div>
{% endif %}
</div>
<div>
{% if items %}
<div id="search-list" {% if not products_as_list -%} class="row" {%- endif %}>
{% for i in range(0, page_length) %}
{% if items[i] %}
{{ items[i] }}
<div class="row">
<div class="col-md-8">
{% if items %}
<div id="search-list">
{% for i in range(0, page_length) %}
{% if items[i] %}
{%- set item = items[i] %}
{% include "erpnext/www/all-products/item_row.html" %}
{% endif %}
{% endfor %}
</div>
<div class="item-group-nav-buttons">
{% if frappe.form_dict.start|int > 0 %}
<a class="btn btn-outline-secondary" href="/{{ pathname }}?start={{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</a>
{% endif %}
{% endfor %}
</div>
<div class="text-center item-group-nav-buttons">
{% if frappe.form_dict.start|int > 0 %}
<a class="btn btn-default" href="/{{ pathname }}?start={{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</a>
{% endif %}
{% if items|length > page_length %}
<a class="btn btn-default" href="/{{ pathname }}?start={{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</a>
{% if items|length > page_length %}
<a class="btn btn-outline-secondary" href="/{{ pathname }}?start={{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</a>
{% endif %}
</div>
{% else %}
<div class="text-muted">{{ _("No items listed") }}.</div>
{% endif %}
</div>
{% else %}
<div class="text-muted">{{ _("No items listed") }}.</div>
{% endif %}
</div>
</div>
{% endblock %}

12
erpnext/templates/includes/address_row.html

@ -1,12 +1,12 @@
<div class="web-list-item">
<a href="/addresses?name={{ doc.name | urlencode }}" class="no-decoration">
<div class="web-list-item mb-3">
<a href="/addresses?name={{ doc.name | urlencode }}" class="no-underline text-reset">
<div class="row">
<div class="col-xs-3">
<div class="col-3">
<span class="indicator {{ "red" if doc.address_type=="Office" else "green" if doc.address_type=="Billing" else "blue" if doc.address_type=="Shipping" else "darkgrey" }}">{{ doc.address_title }}</span>
</div>
<div class="col-xs-2"> {{ _(doc.address_type) }} </div>
<div class="col-xs-2"> {{ doc.city }} </div>
<div class="col-xs-5 text-right small text-muted">
<div class="col-2"> {{ _(doc.address_type) }} </div>
<div class="col-2"> {{ doc.city }} </div>
<div class="col-5 text-right small text-muted">
{{ frappe.get_doc(doc).get_display() }}
</div>
</div>

99
erpnext/templates/includes/cart.js

@ -16,41 +16,33 @@ $.extend(shopping_cart, {
bind_events: function() {
shopping_cart.bind_address_select();
shopping_cart.bind_place_order();
shopping_cart.bind_request_quotation();
shopping_cart.bind_change_qty();
shopping_cart.bind_change_notes();
shopping_cart.bind_dropdown_cart_buttons();
},
bind_address_select: function() {
$(".cart-addresses").find('input[data-address-name]').on("click", function() {
if($(this).prop("checked")) {
var me = this;
// uncheck other shipping or billing addresses:
if ( $(this).is('input[data-fieldname=customer_address]') ) {
$('input[data-fieldname=customer_address]').not(this).prop('checked', false);
} else {
$('input[data-fieldname=shipping_address_name]').not(this).prop('checked', false);
}
return frappe.call({
type: "POST",
method: "erpnext.shopping_cart.cart.update_cart_address",
freeze: true,
args: {
address_fieldname: $(this).attr("data-fieldname"),
address_name: $(this).attr("data-address-name")
},
callback: function(r) {
if(!r.exc) {
$(".cart-tax-items").html(r.message.taxes);
}
$(".cart-addresses").on('click', '.address-card', function(e) {
const $card = $(e.currentTarget);
const address_fieldname = $card.closest('[data-fieldname]').attr('data-fieldname');
const address_name = $card.closest('[data-address-name]').attr('data-address-name');
return frappe.call({
type: "POST",
method: "erpnext.shopping_cart.cart.update_cart_address",
freeze: true,
args: {
address_fieldname,
address_name
},
callback: function(r) {
if(!r.exc) {
$(".cart-tax-items").html(r.message.taxes);
}
});
} else {
return false;
}
}
});
});
},
bind_place_order: function() {
@ -59,12 +51,18 @@ $.extend(shopping_cart, {
});
},
bind_request_quotation: function() {
$('.btn-request-for-quotation').on('click', function() {
shopping_cart.request_quotation(this);
});
},
bind_change_qty: function() {
// bind update button
$(".cart-items").on("change", ".cart-qty", function() {
var item_code = $(this).attr("data-item-code");
var newVal = $(this).val();
shopping_cart.shopping_cart_update(item_code, newVal);
shopping_cart.shopping_cart_update({item_code, qty: newVal});
});
$(".cart-items").on('click', '.number-spinner button', function () {
@ -82,7 +80,21 @@ $.extend(shopping_cart, {
}
input.val(newVal);
var item_code = input.attr("data-item-code");
shopping_cart.shopping_cart_update(item_code, newVal);
shopping_cart.shopping_cart_update({item_code, qty: newVal});
});
},
bind_change_notes: function() {
$('.cart-items').on('change', 'textarea', function() {
const $textarea = $(this);
const item_code = $textarea.attr('data-item-code');
const qty = $textarea.closest('tr').find('.cart-qty').val();
const notes = $textarea.val();
shopping_cart.shopping_cart_update({
item_code,
qty,
additional_notes: notes
});
});
},
@ -150,7 +162,32 @@ $.extend(shopping_cart, {
.html(msg || frappe._("Something went wrong!"))
.toggle(true);
} else {
window.location.href = "/orders/" + encodeURIComponent(r.message);
window.open('/orders/' + encodeURIComponent(r.message), '_blank');
window.location.reload();
}
}
});
},
request_quotation: function(btn) {
return frappe.call({
type: "POST",
method: "erpnext.shopping_cart.cart.request_for_quotation",
btn: btn,
callback: function(r) {
if(r.exc) {
var msg = "";
if(r._server_messages) {
msg = JSON.parse(r._server_messages || []).join("<br>");
}
$("#cart-error")
.empty()
.html(msg || frappe._("Something went wrong!"))
.toggle(true);
} else {
window.open('/printview?doctype=Quotation&name=' + r.message, '_blank');
window.location.reload();
}
}
});

12
erpnext/templates/includes/cart/address_card.html

@ -0,0 +1,12 @@
<div class="card address-card h-100">
<div class="check" style="position: absolute; right: 15px; top: 15px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
<div class="card-body">
<h5 class="card-title">{{ address.name }}</h5>
<p class="card-text text-muted">
{{ address.display }}
</p>
<a href="/addresses?name={{address.name}}" class="card-link">{{ _('Edit') }}</a>
</div>
</div>

159
erpnext/templates/includes/cart/cart_address.html

@ -1,26 +1,141 @@
{% from "erpnext/templates/includes/cart/cart_macros.html" import show_address %}
<div class="row">
{% if addresses|length == 1%}
{% set select_address = True %}
{% endif %}
<div class="col-sm-6">
<div class="h6 text-uppercase">{{ _("Shipping Address") }}</div>
<div id="cart-shipping-address" class="panel-group"
data-fieldname="shipping_address_name">
{% for address in shipping_addresses %}
{{ show_address(address, doc, "shipping_address_name", select_address) }}
{% endfor %}
</div>
<a class="btn btn-default btn-sm" href="/addresses">
{{ _("Manage Addresses") }}</a>
{% if addresses | length == 1%}
{% set select_address = True %}
{% endif %}
<div class="mb-3" data-section="shipping-address">
<h6 class="text-uppercase">{{ _("Shipping Address") }}</h6>
<div class="row no-gutters" data-fieldname="shipping_address_name">
{% for address in shipping_addresses %}
<div class="mr-3 mb-3 w-25" data-address-name="{{address.name}}" {% if doc.shipping_address_name == address.name %} data-active {% endif %}>
{% include "templates/includes/cart/address_card.html" %}
</div>
{% endfor %}
</div>
<div class="col-sm-6">
<div class="h6 text-uppercase">{{ _("Billing Address") }}</div>
<div id="cart-billing-address" class="panel-group"
data-fieldname="customer_address">
{% for address in billing_addresses %}
{{ show_address(address, doc, "customer_address", select_address) }}
{% endfor %}
</div>
</div>
<div class="mb-3" data-section="billing-address">
<h6 class="text-uppercase">{{ _("Billing Address") }}</h6>
<div class="row no-gutters" data-fieldname="customer_address">
{% for address in billing_addresses %}
<div class="mr-3 mb-3 w-25" data-address-name="{{address.name}}" {% if doc.customer_address == address.name %} data-active {% endif %}>
{% include "templates/includes/cart/address_card.html" %}
</div>
{% endfor %}
</div>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="input_same_billing" checked>
<label class="custom-control-label" for="input_same_billing">{{ _('Billing Address is same as Shipping Address') }}</label>
</div>
<button class="btn btn-outline-primary btn-sm mt-3 btn-new-address">{{ _("Add a new address") }}</button>
<script>
frappe.ready(() => {
$(document).on('click', '.address-card', (e) => {
const $target = $(e.currentTarget);
const $section = $target.closest('[data-section]');
$section.find('.address-card').removeClass('active');
$target.addClass('active');
});
$('#input_same_billing').change((e) => {
const $check = $(e.target);
toggle_billing_address_section(!$check.is(':checked'));
});
$('.btn-new-address').click(() => {
const d = new frappe.ui.Dialog({
title: __('New Address'),
fields: [
{
label: __('Address Title'),
fieldname: 'address_title',
fieldtype: 'Data',
reqd: 1
},
{
label: __('Address Type'),
fieldname: 'address_type',
fieldtype: 'Select',
options: [
'Billing',
'Shipping'
],
reqd: 1
},
{
label: __('Address Line 1'),
fieldname: 'address_line1',
fieldtype: 'Data',
reqd: 1
},
{
label: __('Address Line 2'),
fieldname: 'address_line2',
fieldtype: 'Data'
},
{
label: __('City/Town'),
fieldname: 'city',
fieldtype: 'Data',
reqd: 1
},
{
label: __('State'),
fieldname: 'state',
fieldtype: 'Data'
},
{
label: __('Pin Code'),
fieldname: 'pincode',
fieldtype: 'Data'
},
{
label: __('Country'),
fieldname: 'country',
fieldtype: 'Data',
reqd: 1
},
],
primary_action_label: __('Save'),
primary_action: (values) => {
frappe.call('erpnext.shopping_cart.cart.add_new_address', { doc: values })
.then(r => {
d.hide();
window.location.reload();
});
}
})
d.show();
});
function setup_state() {
const shipping_address = $('[data-section="shipping-address"]')
.find('[data-address-name][data-active]').attr('data-address-name');
const billing_address = $('[data-section="billing-address"]')
.find('[data-address-name][data-active]').attr('data-address-name');
$('#input_same_billing').prop('checked', shipping_address === billing_address).trigger('change');
if (!shipping_address && !billing_address) {
$('#input_same_billing').prop('checked', true).trigger('change');
}
if (shipping_address) {
$(`[data-section="shipping-address"] [data-address-name="${shipping_address}"] .address-card`).addClass('active');
}
if (billing_address) {
$(`[data-section="billing-address"] [data-address-name="${billing_address}"] .address-card`).addClass('active');
}
}
setup_state();
function toggle_billing_address_section(flag) {
$('[data-section="billing-address"]').toggle(flag);
}
});
</script>

69
erpnext/templates/includes/cart/cart_items.html

@ -1,31 +1,42 @@
{% from "erpnext/templates/includes/order/order_macros.html" import item_name_and_description %}
{% from "erpnext/templates/includes/order/order_macros.html" import item_name_and_description_cart %}
{% for d in doc.items %}
<div class="row checkout">
<div class="col-sm-8 col-xs-6 col-name-description">
{{ item_name_and_description(d) }}
</div>
<div class="col-sm-2 col-xs-3 text-right col-qty">
<span style="display: inline-block">
<div class="input-group number-spinner">
<span class="input-group-btn">
<button class="btn btn-default cart-btn" data-dir="dwn">
</button>
</span>
<input class="form-control text-right cart-qty"
value = "{{ d.get_formatted('qty') }}"
data-item-code="{{ d.item_code }}">
<span class="input-group-btn">
<button class="btn btn-default cart-btn" data-dir="up" style="margin-left:-2px;">
+</button>
</span>
</div>
<tr data-name="{{ d.name }}">
<td>
<div class="font-weight-bold">
{{ d.item_name }}
</div>
<div>
{{ d.item_code }}
</div>
{%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}
{% if variant_of %}
<span class="text-muted">
{{ _('Variant of') }} <a href="{{frappe.db.get_value('Item', variant_of, 'route')}}">{{ variant_of }}</a>
</span>
</div>
<div class="col-sm-2 col-xs-3 text-right col-amount">
{{ d.get_formatted("amount") }}
<p class="text-muted small item-rate">{{ _("Rate") }}&nbsp;{{ d.get_formatted("rate") }}</p>
</div>
</div>
{% endfor %}
{% endif %}
<div class="mt-2">
<textarea data-item-code="{{d.item_code}}" class="form-control" rows="2" placeholder="{{ _('Add notes') }}">{{d.additional_notes or ''}}</textarea>
</div>
</td>
<td class="text-right">
<div class="input-group number-spinner">
<span class="input-group-prepend d-none d-sm-inline-block">
<button class="btn btn-outline-secondary cart-btn" data-dir="dwn"></button>
</span>
<input class="form-control text-right cart-qty border-secondary" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}">
<span class="input-group-append d-none d-sm-inline-block">
<button class="btn btn-outline-secondary cart-btn" data-dir="up">+</button>
</span>
</div>
</td>
{% if cart_settings.enable_checkout %}
<td class="text-right">
<div>
{{ d.get_formatted('amount') }}
</div>
<span class="text-muted">
{{ _('Rate:') }} {{ d.get_formatted('rate') }}
</span>
</td>
{% endif %}
</tr>
{% endfor %}

17
erpnext/templates/includes/footer/footer_extension.html

@ -1,11 +1,14 @@
{% if not hide_footer_signup %}
<div class='input-group input-group-sm pull-right footer-subscribe'>
<input class="form-control" type="text" id="footer-subscribe-email"
placeholder="{{ _('Your email address') }}...">
<span class='input-group-btn'>
<button class="btn btn-default" type="button"
id="footer-subscribe-button">{{ _("Get Updates") }}</button>
</span>
<div class="input-group">
<input type="text" class="form-control border-secondary"
id="footer-subscribe-email"
placeholder="{{ _('Your email address...') }}"
aria-label="{{ _('Your email address...') }}"
aria-describedby="footer-subscribe-button">
<div class="input-group-append">
<button class="btn btn-outline-secondary"
type="button" id="footer-subscribe-button">{{ _("Get Updates") }}</button>
</div>
</div>
<script>

3
erpnext/templates/includes/footer/footer_powered.html

@ -1,2 +1 @@
<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">
Powered by ERPNext</a>
<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext</a>

43
erpnext/templates/includes/macros.html

@ -1,7 +1,4 @@
{% macro product_image_square(website_image, css_class="") %}
{% if website_image -%}
<meta itemprop="image" content="{{ frappe.utils.quoted(website_image) | abs_url }}"></meta>
{%- endif %}
<div class="product-image product-image-square
{% if not website_image -%} missing-image {%- endif %} {{ css_class }}"
{% if website_image -%}
@ -11,12 +8,8 @@
{% endmacro %}
{% macro product_image(website_image, css_class="") %}
<div class="product-image {% if not website_image -%} missing-image {%- endif %} {{ css_class }}">
{% if website_image -%}
<a href="{{ frappe.utils.quoted(website_image) }}">
<img itemprop="image" src="{{ frappe.utils.quoted(website_image) | abs_url }}" class="img-responsive">
</a>
{%- endif %}
<div class="border text-center rounded h-100 {{ css_class }}" style="overflow: hidden;">
<img itemprop="image" class="website-image h-100 w-100" src="{{ frappe.utils.quoted(website_image or 'no-image.jpg') | abs_url }}">
</div>
{% endmacro %}
@ -33,3 +26,35 @@
{%- endif %}
</div>
{% endmacro %}
{% macro render_homepage_section(section) %}
{% if section.section_based_on == 'Custom HTML' and section.section_html %}
{{ section.section_html }}
{% elif section.section_based_on == 'Cards' %}
<section class="container my-5">
<h3>{{ section.name }}</h3>
<div class="row">
{% for card in section.section_cards %}
<div class="col-md-{{ section.column_value }} mb-4">
<div class="card h-100 justify-content-between">
{% if card.image %}
<div class="website-image-lazy" data-class="card-img-top h-100" data-src="{{ card.image }}" data-alt="{{ card.title }}"></div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ card.title }}</h5>
<p class="card-subtitle mb-2 text-muted">{{ card.subtitle or '' }}</p>
<p class="card-text">{{ card.content | truncate(140, True) }}</p>
</div>
<div class="card-body flex-grow-0">
<a href="{{ card.route }}" class="card-link">{{ _('More details') }}</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% endmacro %}

12
erpnext/templates/includes/navbar/navbar_items.html

@ -1,12 +1,10 @@
{% extends 'frappe/templates/includes/navbar/navbar_items.html' %}
{% block navbar_right_extension %}
<li class="shopping-cart hidden">
<div class="cart-icon">
<a class="dropdown-toggle" href="#" data-toggle="dropdown" id="navLogin">
{{ _("Cart") }} <span class="badge-wrapper" id="cart-count"></span>
</a>
<div id="cart-overlay" class="dropdown-menu shopping-cart-menu"></div>
</div>
<li class="shopping-cart cart-icon hidden">
<a href="/cart" class="nav-link">
{{ _("Cart") }}
<span class="badge badge-primary" id="cart-count"></span>
</a>
</li>
{% endblock %}

8
erpnext/templates/includes/order/order_macros.html

@ -9,7 +9,9 @@
</div>
<div class="col-xs-8 col-sm-10">
{{ d.item_code }}
<div class="text-muted small item-description">{{ d.description }}</div>
<div class="text-muted small item-description">
{{ html2text(d.description) | truncate(140) }}
</div>
</div>
</div>
{% endmacro %}
@ -25,14 +27,14 @@
{{ d.item_name|truncate(25) }}
<div class="input-group number-spinner">
<span class="input-group-btn">
<button class="btn btn-default cart-btn" data-dir="dwn">
<button class="btn btn-light cart-btn" data-dir="dwn">
</button>
</span>
<input class="form-control text-right cart-qty"
value = "{{ d.get_formatted('qty') }}"
data-item-code="{{ d.item_code }}">
<span class="input-group-btn">
<button class="btn btn-default cart-btn" data-dir="up">
<button class="btn btn-light cart-btn" data-dir="up">
+</button>
</span>
</div>

44
erpnext/templates/includes/order/order_taxes.html

@ -1,24 +1,32 @@
{% if doc.taxes %}
<div class="row tax-net-total-row">
<div class="col-xs-6 text-right">{{ _("Net Total") }}</div>
<div class="col-xs-6 text-right">
{{ doc.get_formatted("net_total") }}</div>
</div>
<tr>
<td class="text-right" colspan="2">
{{ _("Net Total") }}
</td>
<td class="text-right">
{{ doc.get_formatted("net_total") }}
</td>
</tr>
{% endif %}
{% for d in doc.taxes %}
{% if d.base_tax_amount > 0 %}
<div class="row tax-row">
<div class="col-xs-6 text-right">{{ d.description }}</div>
<div class="col-xs-6 text-right">
{{ d.get_formatted("base_tax_amount") }}</div>
</div>
<tr>
<td class="text-right" colspan="2">
{{ d.description }}
</td>
<td class="text-right">
{{ d.get_formatted("base_tax_amount") }}
</td>
</tr>
{% endif %}
{% endfor %}
<div class="row tax-grand-total-row">
<div class="col-xs-6 text-right text-uppercase h6 text-muted">{{ _("Grand Total") }}</div>
<div class="col-xs-6 text-right">
<span class="tax-grand-total bold">
{{ doc.get_formatted("grand_total") }}
</span>
</div>
</div>
<tr>
<th class="text-right" colspan="2">
{{ _("Grand Total") }}
</th>
<th class="text-right">
{{ doc.get_formatted("grand_total") }}
</th>
</tr>

215
erpnext/templates/includes/product_page.js

@ -1,215 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.ready(function() {
window.item_code = $('[itemscope] [itemprop="productID"]').text().trim();
var qty = 0;
frappe.call({
type: "POST",
method: "erpnext.shopping_cart.product_info.get_product_info_for_website",
args: {
item_code: get_item_code()
},
callback: function(r) {
if(r.message) {
if(r.message.cart_settings.enabled) {
$(".item-cart, .item-price, .item-stock").toggleClass("hide", (!!!r.message.product_info.price || !!!r.message.product_info.in_stock));
}
if(r.message.cart_settings.show_price) {
$(".item-price").toggleClass("hide", false);
}
if(r.message.cart_settings.show_stock_availability) {
$(".item-stock").toggleClass("hide", false);
}
if(r.message.product_info.price) {
$(".item-price")
.html(r.message.product_info.price.formatted_price_sales_uom + "<div style='font-size: small'>\
(" + r.message.product_info.price.formatted_price + " / " + r.message.product_info.uom + ")</div>");
if(r.message.product_info.in_stock==0) {
$(".item-stock").html("<div style='color: red'> <i class='fa fa-close'></i> {{ _("Not in stock") }}</div>");
}
else if(r.message.product_info.in_stock==1) {
var qty_display = "{{ _("In stock") }}";
if (r.message.product_info.show_stock_qty) {
qty_display += " ("+r.message.product_info.stock_qty+")";
}
$(".item-stock").html("<div style='color: green'>\
<i class='fa fa-check'></i> "+qty_display+"</div>");
}
if(r.message.product_info.qty) {
qty = r.message.product_info.qty;
toggle_update_cart(r.message.product_info.qty);
} else {
toggle_update_cart(0);
}
}
}
}
})
$("#item-add-to-cart button").on("click", function() {
frappe.provide('erpnext.shopping_cart');
erpnext.shopping_cart.update_cart({
item_code: get_item_code(),
qty: $("#item-spinner .cart-qty").val(),
callback: function(r) {
if(!r.exc) {
toggle_update_cart(1);
qty = 1;
}
},
btn: this,
});
});
$("#item-spinner").on('click', '.number-spinner button', function () {
var btn = $(this),
input = btn.closest('.number-spinner').find('input'),
oldValue = input.val().trim(),
newVal = 0;
if (btn.attr('data-dir') == 'up') {
newVal = parseInt(oldValue) + 1;
} else if (btn.attr('data-dir') == 'dwn') {
if (parseInt(oldValue) > 1) {
newVal = parseInt(oldValue) - 1;
}
else {
newVal = parseInt(oldValue);
}
}
input.val(newVal);
});
$("[itemscope] .item-view-attribute .form-control").on("change", function() {
try {
var item_code = encodeURIComponent(get_item_code());
} catch(e) {
// unable to find variant
// then chose the closest available one
var attribute = $(this).attr("data-attribute");
var attribute_value = $(this).val();
var item_code = find_closest_match(attribute, attribute_value);
if (!item_code) {
frappe.msgprint(__("Cannot find a matching Item. Please select some other value for {0}.", [attribute]))
throw e;
}
}
if (window.location.search == ("?variant=" + item_code) || window.location.search.includes(item_code)) {
return;
}
window.location.href = window.location.pathname + "?variant=" + item_code;
});
// change the item image src when alternate images are hovered
$(document.body).on('mouseover', '.item-alternative-image', (e) => {
const $alternative_image = $(e.currentTarget);
const src = $alternative_image.find('img').prop('src');
$('.item-image img').prop('src', src);
});
});
var toggle_update_cart = function(qty) {
$("#item-add-to-cart").toggle(qty ? false : true);
$("#item-update-cart")
.toggle(qty ? true : false)
.find("input").val(qty);
$("#item-spinner").toggle(qty ? false : true);
}
function get_item_code() {
var variant_info = window.variant_info;
if(variant_info) {
var attributes = get_selected_attributes();
var no_of_attributes = Object.keys(attributes).length;
for(var i in variant_info) {
var variant = variant_info[i];
if (variant.attributes.length < no_of_attributes) {
// the case when variant has less attributes than template
continue;
}
var match = true;
for(var j in variant.attributes) {
if(attributes[variant.attributes[j].attribute]
!= variant.attributes[j].attribute_value
) {
match = false;
break;
}
}
if(match) {
return variant.name;
}
}
throw "Unable to match variant";
} else {
return window.item_code;
}
}
function find_closest_match(selected_attribute, selected_attribute_value) {
// find the closest match keeping the selected attribute in focus and get the item code
var attributes = get_selected_attributes();
var previous_match_score = 0;
var previous_no_of_attributes = 0;
var matched;
var variant_info = window.variant_info;
for(var i in variant_info) {
var variant = variant_info[i];
var match_score = 0;
var has_selected_attribute = false;
for(var j in variant.attributes) {
if(attributes[variant.attributes[j].attribute]===variant.attributes[j].attribute_value) {
match_score = match_score + 1;
if (variant.attributes[j].attribute==selected_attribute && variant.attributes[j].attribute_value==selected_attribute_value) {
has_selected_attribute = true;
}
}
}
if (has_selected_attribute
&& ((match_score > previous_match_score) || (match_score==previous_match_score && previous_no_of_attributes < variant.attributes.length))) {
previous_match_score = match_score;
matched = variant;
previous_no_of_attributes = variant.attributes.length;
}
}
if (matched) {
for (var j in matched.attributes) {
var attr = matched.attributes[j];
$('[itemscope]')
.find(repl('.item-view-attribute .form-control[data-attribute="%(attribute)s"]', attr))
.val(attr.attribute_value);
}
return matched.name;
}
}
function get_selected_attributes() {
var attributes = {};
$('[itemscope]').find(".item-view-attribute .form-control").each(function() {
attributes[$(this).attr('data-attribute')] = $(this).val();
});
return attributes;
}

136
erpnext/templates/pages/cart.html

@ -2,18 +2,25 @@
{% block title %} {{ _("Shopping Cart") }} {% endblock %}
{% block header %}<h2>{{ _("My Cart") }}</h2>{% endblock %}
{% block header %}<h1>{{ _("Shopping Cart") }}</h1>{% endblock %}
<!--
{% block script %}
<script>{% include "templates/includes/cart.js" %}</script>
{% endblock %}
-->
{% block header_actions %}
{% if doc.items %}
<button class="btn btn-primary btn-place-order btn-sm"
type="button">
{{ _("Place Order") }}</button>
{% if doc.items and cart_settings.enable_checkout %}
<button class="btn btn-primary btn-place-order" type="button">
{{ _("Place Order") }}
</button>
{% endif %}
{% if doc.items and not cart_settings.enable_checkout %}
<button class="btn btn-primary btn-request-for-quotation" type="button">
{{ _("Request for Quotation") }}
</button>
{% endif %}
{% endblock %}
@ -22,58 +29,89 @@
{% from "templates/includes/macros.html" import item_name_and_description %}
<div class="cart-container">
<div id="cart-container">
<div id="cart-error" class="alert alert-danger"
style="display: none;"></div>
<div id="cart-items">
<div class="row cart-item-header text-muted">
<div class="col-sm-8 col-xs-6 h6 text-uppercase">
{{ _("Item") }}
</div>
<div class="col-sm-2 col-xs-3 text-center h6 text-uppercase">
{{ _("Qty") }}
</div>
<div class="col-sm-2 col-xs-3 text-right h6 text-uppercase">
{{ _("Subtotal") }}
</div>
</div>
{% if doc.items %}
<div class="cart-items">
{% include "templates/includes/cart/cart_items.html" %}
</div>
{% else %}
<p class="empty-cart">{{ _("Cart is Empty") }}</p>
{% endif %}
</div>
{% if doc.items %}
<!-- taxes -->
<div class="row cart-taxes">
<div class="col-sm-6"><!-- empty --></div>
<div class="col-sm-6 text-right cart-tax-items">
{% include "templates/includes/order/order_taxes.html" %}
</div>
</div>
<div id="cart-error" class="alert alert-danger" style="display: none;"></div>
{% if doc.tc_name %}
<div class="cart-terms" style="display: none;" title={{doc.tc_name}}>
{{doc.tc_name}}
{{doc.terms}}
</div>
<div class="cart-link">
<a href="#" onclick="show_terms();return false;">*{{ __("Terms and Conditions") }}</a>
</div>
{% if doc.items %}
<table class="table table-bordered mt-3">
<thead>
<tr>
<th width="60%">{{ _('Item') }}</th>
<th width="20%" class="text-right">{{ _('Quantity') }}</th>
{% if cart_settings.enable_checkout %}
<th width="20%" class="text-right">{{ _('Subtotal') }}</th>
{% endif %}
</tr>
</thead>
<tbody class="cart-items">
{% include "templates/includes/cart/cart_items.html" %}
</tbody>
{% if cart_settings.enable_checkout %}
<tfoot class="cart-tax-items">
{% include "templates/includes/order/order_taxes.html" %}
</tfoot>
{% endif %}
</table>
{% else %}
<p class="text-muted">{{ _('Your cart is Empty') }}</p>
{% endif %}
<div class="cart-addresses">
{% include "templates/includes/cart/cart_address.html" %}
{% if doc.items %}
{% if doc.tc_name %}
<div class="terms-and-conditions-link">
<a href class="link-terms-and-conditions" data-terms-name="{{ doc.tc_name }}">
{{ _("Terms and Conditions") }}
</a>
<script>
frappe.ready(() => {
$('.link-terms-and-conditions').click((e) => {
e.preventDefault();
const $link = $(e.target);
const terms_name = $link.attr('data-terms-name');
show_terms_and_conditions(terms_name);
})
});
function show_terms_and_conditions(terms_name) {
frappe.call('erpnext.shopping_cart.cart.get_terms_and_conditions', { terms_name })
.then(r => {
frappe.msgprint({
title: terms_name,
message: r.message
});
});
}
</script>
</div>
{% endif %}
<p class="cart-footer text-right">
<button class="btn btn-primary btn-place-order btn-sm" type="button">
{{ _("Place Order") }}</button></p>
{% if cart_settings.enable_checkout %}
<div class="cart-addresses mt-5">
{% include "templates/includes/cart/cart_address.html" %}
</div>
{% endif %}
{% endif %}
</div>
<div class="row mt-5">
<div class="col-12">
{% if cart_settings.enable_checkout %}
<a href="/orders">
{{ _('See past orders') }}
</a>
{% else %}
<a href="/quotations">
{{ _('See past quotations') }}
</a>
{% endif %}
</div>
</div>
{% endblock %}
{% block base_scripts %}
<!-- js should be loaded in body! -->
<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/assets/js/frappe-web.min.js"></script>
<script type="text/javascript" src="/assets/js/control.min.js"></script>
<script type="text/javascript" src="/assets/js/dialog.min.js"></script>
<script type="text/javascript" src="/assets/js/bootstrap-4-web.min.js"></script>
{% endblock %}

2
erpnext/templates/pages/help.html

@ -11,7 +11,7 @@
value='{{ frappe.form_dict.q or ''}}'
{% if not frappe.form_dict.q%}placeholder="{{ _("What do you need help with?") }}"{% endif %}>
<input type='submit'
class='btn btn-sm btn-default btn-search' value="{{ _("Search") }}">
class='btn btn-sm btn-light btn-search' value="{{ _("Search") }}">
</form>
</div>

9
erpnext/templates/pages/home.css

@ -0,0 +1,9 @@
/* csslint ignore:start */
{% if homepage.hero_image %}
.hero-image {
background-image: url("{{ homepage.hero_image }}");
background-size: cover;
padding: 10rem 0;
}
{% endif %}
/* csslint ignore:end */

130
erpnext/templates/pages/home.html

@ -1,75 +1,75 @@
{% extends "templates/web.html" %}
{% from "erpnext/templates/includes/macros.html" import product_image_square %}
{% block page_content %}
{% from "erpnext/templates/includes/macros.html" import render_homepage_section %}
<div class="row">
<div class="col-sm-12">
<div class="hero">
<h1 class="text-center">{{ homepage.tag_line or '' }}</h1>
<p class="text-center">{{ homepage.description or '' }}</p>
{% block content %}
<main>
{% if homepage.hero_section_based_on == 'Default' %}
<section class="hero-section border-bottom {%if homepage.hero_image%}hero-image{%endif%}">
<div class="container py-5">
<h1 class="d-none d-sm-block display-4">{{ homepage.tag_line }}</h1>
<h1 class="d-block d-sm-none">{{ homepage.tag_line }}</h1>
<h2 class="d-none d-sm-block">{{ homepage.description }}</h2>
<h3 class="d-block d-sm-none">{{ homepage.description }}</h3>
</div>
{% if homepage.products %}
<div class='featured-products-section' itemscope itemtype="http://schema.org/Product">
<h5 class='featured-product-heading'>{{ _("Featured Products") }}</h5>
<div class="featured-products">
<div id="search-list" class="row" style="margin-top:40px;">
{% for item in homepage.products %}
<a class="product-link" href="{{ item.route|abs_url }}">
<div class="col-sm-4 col-xs-4 product-image-wrapper">
<div class="product-image-img">
<!-- thumbnail not updated, and used as background image in item card -->
{{ product_image_square(item.image) }}
<div class="product-text" itemprop="name">{{ item.item_name }}</div>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
<div class="text-center padding">
<a href="{{ homepage.products_url or "/products" }}" class="btn btn-primary all-products">
{{ _("View All Products") }}</a></div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block style %}
<style>
.hero {
padding-top: 50px;
padding-bottom: 100px;
}
.hero h1 {
font-size: 40px;
font-weight: 200;
}
<div class="container">
<a href="{{ explore_link }}" class="mb-5 btn btn-primary">{{ _('Explore') }}</a>
</div>
</section>
{% elif homepage.hero_section_based_on == 'Slideshow' and slideshow %}
<section class="hero-section">
{% include "templates/includes/slideshow.html" %}
</section>
{% elif homepage.hero_section_based_on == 'Homepage Section' %}
{{ render_homepage_section(homepage.hero_section_doc) }}
{% endif %}
.home-login {
margin-top: 30px;
}
.btn-login {
width: 80px;
}
{% if homepage.products %}
<section class="container section-products my-5">
<h3>{{ _('Products') }}</h3>
.featured-product-heading, .all-products {
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 12px;
font-weight: 500;
}
<div class="row">
{% for item in homepage.products %}
<div class="col-md-4 mb-4">
<div class="card h-100 justify-content-between">
<div class="website-image-lazy" data-class="card-img-top h-100" data-src="{{ item.image }}" data-alt="{{ item.item_name }}"></div>
<div class="card-body flex-grow-0">
<h5 class="card-title">{{ item.item_name }}</h5>
<a href="{{ item.route }}" class="card-link">{{ _('More details') }}</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
.all-products {
font-weight: 300;
padding-left: 25px;
padding-right: 25px;
padding-top: 10px;
padding-bottom: 10px;
}
{% if blogs %}
<section class="container my-5">
<h3>{{ _('Publications') }}</h3>
<div class="row">
{% for blog in blogs %}
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ blog.title }}</h5>
<p class="card-subtitle mb-2 text-muted">{{ _('By {0}').format(blog.blogger) }}</p>
<p class="card-text">{{ blog.blog_intro }}</p>
</div>
<div class="card-body flex-grow-0">
<a href="{{ blog.route }}" class="card-link">{{ _('Read blog') }}</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
</style>
{% endblock %}
{% for section in homepage_sections %}
{{ render_homepage_section(section) }}
{% endfor %}
</main>
{% endblock %}

43
erpnext/templates/pages/home.py

@ -15,15 +15,38 @@ def get_context(context):
if route:
item.route = '/' + route
context.title = homepage.title or homepage.company
homepage.title = homepage.title or homepage.company
context.title = homepage.title
context.homepage = homepage
# show atleast 3 products
if len(homepage.products) < 3:
for i in range(3 - len(homepage.products)):
homepage.append('products', {
'item_code': 'product-{0}'.format(i),
'item_name': frappe._('Product {0}').format(i),
'route': '#'
})
if homepage.hero_section_based_on == 'Homepage Section' and homepage.hero_section:
homepage.hero_section_doc = frappe.get_doc('Homepage Section', homepage.hero_section)
context.homepage = homepage
if homepage.slideshow:
doc = frappe.get_doc('Website Slideshow', homepage.slideshow)
context.slideshow = homepage.slideshow
context.slideshow_header = doc.header
context.slides = doc.slideshow_items
context.blogs = frappe.get_all('Blog Post',
fields=['title', 'blogger', 'blog_intro', 'route'],
filters={
'published': 1
},
order_by='modified desc',
limit=3
)
# filter out homepage section which is used as hero section
homepage_hero_section = homepage.hero_section_based_on == 'Homepage Section' and homepage.hero_section
homepage_sections = frappe.get_all('Homepage Section',
filters=[['name', '!=', homepage_hero_section]] if homepage_hero_section else None,
order_by='section_order asc'
)
context.homepage_sections = [frappe.get_doc('Homepage Section', name) for name in homepage_sections]
context.metatags = context.metatags or frappe._dict({})
context.metatags.image = homepage.hero_image or None
context.metatags.description = homepage.description or None
context.explore_link = '/all-products'

4
erpnext/templates/pages/material_request_info.html

@ -12,7 +12,7 @@
{% endblock %}
{% block header_actions %}
<a class='btn btn-xs btn-default' href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target="_blank" rel="noopener noreferrer">{{ _("Print") }}</a>
<a class='btn btn-xs btn-light' href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target="_blank" rel="noopener noreferrer">{{ _("Print") }}</a>
{% endblock %}
{% block page_content %}
@ -70,5 +70,5 @@
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}

2
erpnext/templates/pages/non_profit/leave-chapter.html

@ -9,7 +9,7 @@
<label for="leave">Why do you want to leave this chapter</label>
<input type="text" name="leave" class="form-control" id="leave">
</div>
<button type="button" class="btn btn-default btn-leave" data-title= "{{ chapter.name }}" id="btn-leave">Submit
<button type="button" class="btn btn-light btn-leave" data-title= "{{ chapter.name }}" id="btn-leave">Submit
</button>
</form>
</div>

40
erpnext/templates/pages/order.html

@ -8,23 +8,22 @@
{% block title %}{{ doc.name }}{% endblock %}
{% block header %}
<h1>{{ doc.name }}</h1>
<h1 class="m-0">{{ doc.name }}</h1>
{% endblock %}
{% block header_actions %}
<a class='btn btn-xs btn-default' href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target="_blank" rel="noopener noreferrer">{{ _("Print") }}</a>
<a href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target="_blank" rel="noopener noreferrer">{{ _("Print") }}</a>
{% endblock %}
{% block page_content %}
<div class="row transaction-subheading">
<div class="col-xs-6">
<div class="col-6">
<span class="indicator {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
{{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
</span>
</div>
<div class="col-xs-6 text-muted text-right small">
<div class="col-6 text-muted text-right small">
{{ frappe.utils.formatdate(doc.transaction_date, 'medium') }}
{% if doc.valid_till %}
<p>
@ -34,16 +33,14 @@
</div>
</div>
<p class='small' style='padding-top: 15px;'>
{% if doc.doctype == 'Supplier Quotation' %}
<b>{{ doc.supplier_name}}</b>
{% else %}
<b>{{ doc.customer_name}}</b>
{% endif %}
{% if doc.contact_display %}
<br>
{{ doc.contact_display }}
{% endif %}
<p class="small my-3">
{%- set party_name = doc.supplier_name if doc.doctype == 'Supplier Quotation' else doc.customer_name %}
<b>{{ party_name }}</b>
{% if doc.contact_display and doc.contact_display != party_name %}
<br>
{{ doc.contact_display }}
{% endif %}
</p>
{% if doc._header %}
@ -55,7 +52,7 @@
<!-- items -->
<div class="order-item-table">
<div class="row order-items order-item-header text-muted">
<div class="col-sm-6 col-xs-6 h6 text-uppercase">
<div class="col-sm-6 col-6 h6 text-uppercase">
{{ _("Item") }}
</div>
<div class="col-sm-3 col-xs-3 text-right h6 text-uppercase">
@ -67,7 +64,7 @@
</div>
{% for d in doc.items %}
<div class="row order-items">
<div class="col-sm-6 col-xs-6">
<div class="col-sm-6 col-6">
{{ item_name_and_description(d) }}
</div>
<div class="col-sm-3 col-xs-3 text-right">
@ -85,11 +82,10 @@
</div>
<!-- taxes -->
<div class="order-taxes row">
<div class="col-sm-6"><!-- empty --></div>
<div class="col-sm-6 text-right">
<div class="order-taxes d-flex justify-content-end">
<table>
{% include "erpnext/templates/includes/order/order_taxes.html" %}
</div>
</table>
</div>
</div>
@ -115,7 +111,7 @@
<div class="control-input">
<input class="form-control" type="number" min="0" max="{{ available_loyalty_points }}" id="loyalty-point-to-redeem">
</div>
<p class="help-box small text-muted hidden-xs"> Available Points: {{ available_loyalty_points }} </p>
<p class="help-box small text-muted d-none d-sm-block"> Available Points: {{ available_loyalty_points }} </p>
</div>
</div>
{% endif %}

2
erpnext/templates/pages/product_search.html

@ -25,7 +25,7 @@ frappe.ready(function() {
<div style="text-align: center;">
<div class="more-btn"
style="display: none; text-align: center;">
<button class="btn btn-default">{{ _("More...") }}</button>
<button class="btn btn-light">{{ _("More...") }}</button>
</div>
</div>
</div>

4
erpnext/templates/pages/projects.html

@ -20,11 +20,11 @@
aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;">
</div>
</div>
{% endif %}
{% endif %}
<div class="clearfix">
<h4 style="float: left;">{{ _("Tasks") }}</h4>
<a class="btn btn-secondary btn-default btn-sm" style="float: right; position: relative; top: 10px;" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
<a class="btn btn-secondary btn-light btn-sm" style="float: right; position: relative; top: 10px;" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
</div>
<p>

4
erpnext/templates/pages/task_info.html

@ -20,7 +20,7 @@
<div class="page-header-actions-block" data-html-block="header-actions">
<button type="submit" class="btn btn-primary btn-sm btn-form-submit">
{{ __("Update") }}</button>
<a href="tasks" class="btn btn-default btn-sm">
<a href="tasks" class="btn btn-light btn-sm">
{{ __("Cancel") }}</a>
</div>
</div>
@ -91,7 +91,7 @@
{% endfor %}
</div>
<div class="comment-form-wrapper">
<a class="add-comment btn btn-default btn-sm">{{ __("Add Comment") }}</a>
<a class="add-comment btn btn-light btn-sm">{{ __("Add Comment") }}</a>
<div style="display: none;" id="comment-form">
<p>{{ __("Add Comment") }}</p>
<form>

0
erpnext/www/all-products/__init__.py

163
erpnext/www/all-products/index.html

@ -0,0 +1,163 @@
{% extends "templates/web.html" %}
{% block title %}{{ _('Products') }}{% endblock %}
{% block header %}
<h1>{{ _('Products') }}</h1>
{% endblock header %}
{% block page_content %}
<div class="row">
<div class="col-8">
<div class="input-group input-group-sm mb-3">
<input type="search" class="form-control" placeholder="{{_('Search')}}"
aria-label="{{_('Product Search')}}" aria-describedby="product-search"
value="{{ frappe.form_dict.search or '' }}"
>
</div>
</div>
<div class="col-4 pl-0">
<button class="btn btn-light btn-sm btn-block d-md-none"
type="button"
data-toggle="collapse"
data-target="#product-filters"
aria-expanded="false"
aria-controls="product-filters"
style="white-space: nowrap;"
>
{{ _('Toggle Filters') }}
</button>
</div>
</div>
<div class="row">
<div class="col-12 order-2 col-md-8 order-md-1 products-list">
{% 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 %}
</div>
<div class="col-12 order-1 col-md-4 order-md-2">
{% if frappe.form_dict.start or frappe.form_dict.field_filters or frappe.form_dict.search %}
<a class="mb-3 d-inline-block" href="/all-products">{{ _('Clear filters') }}</a>
{% endif %}
<div class="collapse d-md-block" id="product-filters">
{% for field_filter in field_filters %}
{%- set item_field = field_filter[0] %}
{%- set values = field_filter[1] %}
<div class="mb-4">
<h6>{{ item_field.label }}</h6>
{% if values | len > 20 %}
<!-- show inline filter if values more than 20 -->
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
{% endif %}
{% if values %}
<div class="filter-options">
{% for value in values %}
<div class="custom-control custom-checkbox" data-value="{{ value }}">
<input type="checkbox"
class="product-filter field-filter custom-control-input"
id="{{value}}"
data-filter-name="{{ item_field.fieldname }}"
data-filter-value="{{ value }}"
>
<label class="custom-control-label" for="{{value}}">
{{ value }}
</label>
</div>
{% endfor %}
</div>
{% else %}
<i class="text-muted">{{ _('No values') }}</i>
{% endif %}
</div>
{% endfor %}
{% for attribute in attribute_filters %}
<div class="mb-4">
<h6>{{ attribute.name }}</h6>
{% if values | len > 20 %}
<!-- show inline filter if values more than 20 -->
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
{% endif %}
{% if attribute.item_attribute_values %}
<div class="filter-options">
{% for attr_value in attribute.item_attribute_values %}
<div class="custom-control custom-checkbox" data-value="{{ value }}">
<input type="checkbox"
class="product-filter attribute-filter custom-control-input"
id="{{attr_value.name}}"
data-attribute-name="{{ attribute.name }}"
data-attribute-value="{{ attr_value.attribute_value }}"
{% if attr_value.checked %} checked {% endif %}
>
<label class="custom-control-label" for="{{attr_value.name}}">
{{ attr_value.attribute_value }}
</label>
</div>
{% endfor %}
</div>
{% else %}
<i class="text-muted">{{ _('No values') }}</i>
{% endif %}
</div>
{% endfor %}
</div>
<script>
frappe.ready(() => {
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
const $input = $(e.target);
const keyword = ($input.val() || '').toLowerCase();
const $filter_options = $input.next('.filter-options');
$filter_options.find('.custom-control').show();
$filter_options.find('.custom-control').each((i, el) => {
const $el = $(el);
const value = $el.data('value').toLowerCase();
if (!value.includes(keyword)) {
$el.hide();
}
});
}, 300));
})
</script>
</div>
</div>
<div class="row">
<div class="col-12">
{% if frappe.form_dict.start|int > 0 %}
<button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
{% endif %}
{% if items|length >= page_length %}
<button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
{% endif %}
</div>
</div>
<script>
frappe.ready(() => {
$('.btn-prev, .btn-next').click((e) => {
const $btn = $(e.target);
$btn.prop('disabled', true);
const start = $btn.data('start');
let query_params = frappe.utils.get_query_params();
query_params.start = start;
let path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);
window.location.href = path;
});
});
</script>
{% endblock %}

161
erpnext/www/all-products/index.js

@ -0,0 +1,161 @@
$(() => {
class ProductListing {
constructor() {
this.bind_filters();
this.bind_search();
this.restore_filters_state();
}
bind_filters() {
this.field_filters = {};
this.attribute_filters = {};
$('.product-filter').on('change', frappe.utils.debounce((e) => {
const $checkbox = $(e.target);
const is_checked = $checkbox.is(':checked');
if ($checkbox.is('.attribute-filter')) {
const {
attributeName: attribute_name,
attributeValue: attribute_value
} = $checkbox.data();
if (is_checked) {
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
this.attribute_filters[attribute_name].push(attribute_value);
} else {
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name].filter(v => v !== attribute_value);
}
if (this.attribute_filters[attribute_name].length === 0) {
delete this.attribute_filters[attribute_name];
}
} else if ($checkbox.is('.field-filter')) {
const {
filterName: filter_name,
filterValue: filter_value
} = $checkbox.data();
if (is_checked) {
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
this.field_filters[filter_name].push(filter_value);
} else {
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
this.field_filters[filter_name] = this.field_filters[filter_name].filter(v => v !== filter_value);
}
if (this.field_filters[filter_name].length === 0) {
delete this.field_filters[filter_name];
}
}
const query_string = get_query_string({
field_filters: JSON.stringify(if_key_exists(this.field_filters)),
attribute_filters: JSON.stringify(if_key_exists(this.attribute_filters)),
});
window.history.pushState('filters', '', '/all-products?' + query_string);
$('.page_content input').prop('disabled', true);
this.get_items_with_filters()
.then(html => {
$('.products-list').html(html);
})
.then(data => {
$('.page_content input').prop('disabled', false);
return data;
})
.catch(() => {
$('.page_content input').prop('disabled', false);
});
}, 1000));
}
make_filters() {
}
bind_search() {
$('input[type=search]').on('keydown', (e) => {
if (e.keyCode === 13) {
// Enter
const value = e.target.value;
if (value) {
window.location.search = 'search=' + e.target.value;
} else {
window.location.search = '';
}
}
});
}
restore_filters_state() {
const filters = frappe.utils.get_query_params();
let {field_filters, attribute_filters} = filters;
if (field_filters) {
field_filters = JSON.parse(field_filters);
for (let fieldname in field_filters) {
const values = field_filters[fieldname];
const selector = values.map(value => {
return `input[data-filter-name="${fieldname}"][data-filter-value="${value}"]`;
}).join(',');
$(selector).prop('checked', true);
}
this.field_filters = field_filters;
}
if (attribute_filters) {
attribute_filters = JSON.parse(attribute_filters);
for (let attribute in attribute_filters) {
const values = attribute_filters[attribute];
const selector = values.map(value => {
return `input[data-attribute-name="${attribute}"][data-attribute-value="${value}"]`;
}).join(',');
$(selector).prop('checked', true);
}
this.attribute_filters = attribute_filters;
}
}
get_items_with_filters() {
const { attribute_filters, field_filters } = this;
const args = {
field_filters: if_key_exists(field_filters),
attribute_filters: if_key_exists(attribute_filters)
};
return new Promise((resolve, reject) => {
frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args)
.then(r => {
if (r.exc) reject(r.exc);
else resolve(r.message);
})
.fail(reject);
});
}
}
new ProductListing();
function get_query_string(object) {
const url = new URLSearchParams();
for (let key in object) {
const value = object[key];
if (value) {
url.append(key, value);
}
}
return url.toString();
}
function if_key_exists(obj) {
let exists = false;
for (let key in obj) {
if (obj.hasOwnProperty(key) && obj[key]) {
exists = true;
break;
}
}
return exists ? obj : undefined;
}
});

26
erpnext/www/all-products/index.py

@ -0,0 +1,26 @@
import frappe
from erpnext.portal.product_configurator.utils import (get_products_for_website, get_product_settings,
get_field_filter_data, get_attribute_filter_data)
def get_context(context):
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)
else:
search = field_filters = attribute_filters = None
context.items = get_products_for_website(field_filters, attribute_filters, search)
product_settings = get_product_settings()
context.field_filters = get_field_filter_data() \
if product_settings.enable_field_filters else []
context.attribute_filters = get_attribute_filter_data() \
if product_settings.enable_attribute_filters else []
context.product_settings = product_settings
context.page_length = product_settings.products_per_page
context.no_cache = 1

24
erpnext/www/all-products/item_row.html

@ -0,0 +1,24 @@
<div class="card mb-3">
<div class="row no-gutters">
<div class="col-md-3">
<div class="card-body">
<a class="no-underline" href="{{ item.route }}">
<img class="website-image" src="{{ item.website_image or item.image or 'no-image.jpg' }}" alt="{{ item.item_name }}">
</a>
</div>
</div>
<div class="col-md-9">
<div class="card-body">
<h5 class="card-title">
<a class="text-dark" href="{{ item.route }}">
{{ item.item_name or item.name }}
</a>
</h5>
<p class="card-text">
{{ item.website_description or item.description or '<i class="text-muted">No description</i>' }}
</p>
<a href="{{ item.route }}" class="btn btn-sm btn-light">{{ _('More details') }}</a>
</div>
</div>
</div>
</div>

1
erpnext/www/all-products/not_found.html

@ -0,0 +1 @@
<div class="d-flex justify-content-center p-3 text-muted">{{ _('No products found') }}</div>
Loading…
Cancel
Save