Browse Source
- 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 fielddevelop
Faris Ansari
6 years ago
committed by
GitHub
93 changed files with 5052 additions and 1617 deletions
@ -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> |
@ -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 |
|||
''') |
@ -0,0 +1,4 @@ |
|||
import frappe |
|||
|
|||
def execute(): |
|||
frappe.db.set_value('Homepage', 'Homepage', 'hero_section_based_on', 'Default') |
@ -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,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', { |
|||
|
|||
}); |
@ -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 |
|||
} |
@ -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)) |
@ -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,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 |
|||
} |
@ -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 |
@ -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 |
|||
} |
@ -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 |
|||
} |
@ -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,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 |
|||
} |
@ -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,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') |
@ -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() |
@ -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 |
@ -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";`); |
|||
} |
|||
} |
|||
}); |
@ -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; |
|||
} |
@ -0,0 +1 @@ |
|||
/Users/netchampfaris/frappe-bench/apps/erpnext/node_modules |
@ -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; |
|||
} |
File diff suppressed because it is too large
@ -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', { |
|||
|
|||
}); |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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); |
|||
}); |
|||
}); |
@ -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> |
@ -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> |
@ -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(); |
|||
}); |
|||
}); |
@ -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 %} |
@ -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> |
@ -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> |
|||
|
@ -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") }} {{ 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 %} |
|||
|
@ -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> |
|||
|
@ -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 %} |
@ -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> |
|||
|
@ -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; |
|||
} |
@ -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 */ |
@ -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 %} |
@ -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 %} |
|||
|
|||
|
@ -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; |
|||
} |
|||
}); |
@ -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 |
@ -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> |
@ -0,0 +1 @@ |
|||
<div class="d-flex justify-content-center p-3 text-muted">{{ _('No products found') }}</div> |
Loading…
Reference in new issue