fix: Discount Filters and Web templates

- Fixed discount filters (didn’t work after js render change)
- Fix Item Card Group template height and style
- Add placeholder to missing images in Product Category Cards template
- Code cleanup
This commit is contained in:
marination
2021-05-25 01:35:22 +05:30
parent 82cc7f1027
commit bba0bc874a
15 changed files with 215 additions and 204 deletions

View File

@@ -114,7 +114,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
next_attribute = attribute next_attribute = attribute
break break
valid_options_for_attributes = frappe._dict({}) valid_options_for_attributes = frappe._dict()
for a in attribute_list: for a in attribute_list:
valid_options_for_attributes[a] = set() valid_options_for_attributes[a] = set()

View File

@@ -12,6 +12,7 @@ erpnext.ProductGrid = class {
this.products_section.addClass("hidden"); this.products_section.addClass("hidden");
} }
this.products_section.empty();
this.make(); this.make();
} }

View File

@@ -12,6 +12,7 @@ erpnext.ProductList = class {
this.products_section.addClass("hidden"); this.products_section.addClass("hidden");
} }
this.products_section.empty();
this.make(); this.make();
} }

View File

@@ -103,7 +103,7 @@ class ProductQuery:
def query_items(self, conditions, or_conditions, substitutions, start=0, with_attributes=False): def query_items(self, conditions, or_conditions, substitutions, start=0, with_attributes=False):
"""Build a query to fetch Website Items based on field filters.""" """Build a query to fetch Website Items based on field filters."""
self.query_fields = (", ").join(self.fields) self.query_fields = ", ".join(self.fields)
attribute_table = ", `tabItem Variant Attribute` iva" if with_attributes else "" attribute_table = ", `tabItem Variant Attribute` iva" if with_attributes else ""
@@ -116,9 +116,9 @@ class ProductQuery:
{conditions} {conditions}
{or_conditions} {or_conditions}
limit {self.page_length} offset {start} limit {self.page_length} offset {start}
""", """,
tuple(substitutions), tuple(substitutions),
as_dict=1) as_dict=1)
def query_items_with_attributes(self, attributes, start=0): def query_items_with_attributes(self, attributes, start=0):
"""Build a query to fetch Website Items based on field & attribute filters.""" """Build a query to fetch Website Items based on field & attribute filters."""
@@ -144,7 +144,7 @@ class ProductQuery:
all_items.append(set(items_dict.keys())) all_items.append(set(items_dict.keys()))
result = [items_dict.get(item) for item in list(set.intersection(*all_items))] result = [items_dict.get(item) for item in set.intersection(*all_items)]
return result return result
def build_fields_filters(self, filters): def build_fields_filters(self, filters):
@@ -180,11 +180,8 @@ class ProductQuery:
# Join the meta fields and default fields set # Join the meta fields and default fields set
search_fields = default_fields.union(meta_fields) search_fields = default_fields.union(meta_fields)
try: if frappe.db.count('Item', cache=True) > 50000:
if frappe.db.count('Item', cache=True) > 50000: search_fields.discard('description')
search_fields.remove('description')
except KeyError:
pass
# Build or filters for query # Build or filters for query
search = '%{}%'.format(search_term) search = '%{}%'.format(search_term)

View File

@@ -6,8 +6,11 @@ erpnext.ProductView = class {
*/ */
constructor(options) { constructor(options) {
Object.assign(this, options); Object.assign(this, options);
this.preference = "List View"; this.preference = this.view_type;
this.make();
}
make() {
this.products_section.empty(); this.products_section.empty();
this.prepare_view_toggler(); this.prepare_view_toggler();
this.get_item_filter_data(); this.get_item_filter_data();
@@ -22,12 +25,12 @@ erpnext.ProductView = class {
} }
get_item_filter_data() { get_item_filter_data() {
// Get and render all Items related components // Get and render all Product related views
let me = this; let me = this;
let args = this.get_query_filters(); let args = this.get_query_filters();
$('#list').prop('disabled', true); this.disable_view_toggler(true);
$('#image-view').prop('disabled', true);
frappe.call({ frappe.call({
method: 'erpnext.e_commerce.doctype.website_item.website_item.get_product_filter_data', method: 'erpnext.e_commerce.doctype.website_item.website_item.get_product_filter_data',
args: args, args: args,
@@ -55,14 +58,20 @@ erpnext.ProductView = class {
me.render_no_products_section(); me.render_no_products_section();
} }
$('#list').prop('disabled', false); me.disable_view_toggler(false);
$('#image-view').prop('disabled', false);
} }
}); });
} }
disable_view_toggler(disable=false) {
$('#list').prop('disabled', disable);
$('#image-view').prop('disabled', disable);
}
render_filters(filter_data) { render_filters(filter_data) {
this.get_discount_filter_html(filter_data.discount_filters); this.get_discount_filter_html(filter_data.discount_filters);
this.bind_filters();
this.restore_filters_state();
} }
render_grid_view(items, settings) { render_grid_view(items, settings) {
@@ -226,9 +235,11 @@ erpnext.ProductView = class {
html += ` html += `
<div class="checkbox"> <div class="checkbox">
<label data-value="${ filter[0] }"> <label data-value="${ filter[0] }">
<input type="radio" class="product-filter discount-filter" <input type="radio"
class="product-filter discount-filter"
name="discount" id="${ filter[0] }" name="discount" id="${ filter[0] }"
data-filter-name="discount" data-filter-value="${ filter[0] }" data-filter-name="discount"
data-filter-value="${ filter[0] }"
> >
<span class="label-area" for="${ filter[0] }"> <span class="label-area" for="${ filter[0] }">
${ filter[1] } ${ filter[1] }
@@ -243,6 +254,97 @@ erpnext.ProductView = class {
} }
} }
bind_filters() {
let me = this;
this.field_filters = {};
this.attribute_filters = {};
$('.product-filter').on('change', (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') || $checkbox.is('.discount-filter')) {
const {
filterName: filter_name,
filterValue: filter_value
} = $checkbox.data();
if ($checkbox.is('.discount-filter')) {
// clear previous discount filter to accomodate new
delete this.field_filters["discount"];
}
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];
}
}
let route_params = frappe.utils.get_query_params();
const query_string = get_query_string({
start: if_key_exists(route_params.start) || 0,
field_filters: JSON.stringify(if_key_exists(this.field_filters)),
attribute_filters: JSON.stringify(if_key_exists(this.attribute_filters)),
});
window.history.pushState('filters', '', `${location.pathname}?` + query_string);
$('.page_content input').prop('disabled', true);
me.make();
$('.page_content input').prop('disabled', false);
});
}
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;
}
}
render_no_products_section() { render_no_products_section() {
this.products_section.append(` this.products_section.append(`
<br><br><br> <br><br><br>
@@ -279,3 +381,25 @@ erpnext.ProductView = class {
} }
} }
}; };
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;
}

View File

@@ -6,8 +6,15 @@
}) -%} }) -%}
<div class="card h-100"> <div class="card h-100">
{% if image %} {% if image %}
<img class="card-img-top" src="{{ image }}" alt="{{ title }}"> <img class="card-img-top" src="{{ image }}" alt="{{ title }}" style="max-height: 200px;">
{% else %}
<div class="placeholder-div" style="max-height: 200px;">
<span class="placeholder">
{{ frappe.utils.get_abbr(title or '') }}
</span>
</div>
{% endif %} {% endif %}
<div class="card-body text-center text-muted small"> <div class="card-body text-center text-muted small">
{{ title or '' }} {{ title or '' }}
</div> </div>

View File

@@ -54,12 +54,12 @@ def execute():
for doctype in ("Website Item Group", "Item Website Specification"): for doctype in ("Website Item Group", "Item Website Specification"):
web_item, item = website_item.name, item.item_code web_item, item = website_item.name, item.item_code
frappe.db.sql(f""" frappe.db.sql(f"""
Update `tab{doctype}` Update
`tab{doctype}`
set set
parenttype = 'Website Item', parenttype = 'Website Item',
parent = '{web_item}' parent = '{web_item}'
where where
parenttype = 'Item' parenttype = 'Item'
and parent = '{item}' and parent = '{item}'
""" """)
)

View File

@@ -84,8 +84,8 @@ $.extend(wishlist, {
const $wish_icon = $btn.find('.wish-icon'); const $wish_icon = $btn.find('.wish-icon');
let me = this; let me = this;
if(frappe.session.user==="Guest") { if (frappe.session.user==="Guest") {
if(localStorage) { if (localStorage) {
localStorage.setItem("last_visited", window.location.pathname); localStorage.setItem("last_visited", window.location.pathname);
} }
window.location.href = "/login"; window.location.href = "/login";
@@ -137,7 +137,7 @@ $.extend(wishlist, {
failure_action: method to execute on failure, failure_action: method to execute on failure,
async: make call asynchronously (true/false). */ async: make call asynchronously (true/false). */
if (frappe.session.user==="Guest") { if (frappe.session.user==="Guest") {
if(localStorage) { if (localStorage) {
localStorage.setItem("last_visited", window.location.pathname); localStorage.setItem("last_visited", window.location.pathname);
} }
window.location.href = "/login"; window.location.href = "/login";

View File

@@ -780,3 +780,16 @@ body.product-page {
#toggle-view { #toggle-view {
float: right; float: right;
} }
.placeholder-div {
height:80%;
width: -webkit-fill-available;
padding: 50px;
text-align: center;
background-color: #F9FAFA;
border-top-left-radius: calc(0.75rem - 1px);
border-top-right-radius: calc(0.75rem - 1px);
}
.placeholder {
font-size: 72px;
}

View File

@@ -73,10 +73,10 @@
{% if is_featured %} {% if is_featured %}
<div class="col-sm-{{ col_size*2 }} item-card"> <div class="col-sm-{{ col_size*2 }} item-card">
<div class="card featured-item {{ align_items_class }}"> <div class="card featured-item {{ align_items_class }}" style="height: 360px;">
{% if image %} {% if image %}
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-md-6"> <div class="col-md-5 ml-4">
<img class="card-img" src="{{ image }}" alt="{{ title }}"> <img class="card-img" src="{{ image }}" alt="{{ title }}">
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@@ -92,7 +92,7 @@
</div> </div>
{% else %} {% else %}
<div class="col-sm-{{ col_size }} item-card"> <div class="col-sm-{{ col_size }} item-card">
<div class="card {{ align_items_class }}"> <div class="card {{ align_items_class }}" style="height: 360px;">
{% if image %} {% if image %}
<div class="card-img-container"> <div class="card-img-container">
<a href="/{{ item.route or '#' }}" style="text-decoration: none;"> <a href="/{{ item.route or '#' }}" style="text-decoration: none;">
@@ -119,19 +119,17 @@
'text-left': align == 'Left' or is_featured, 'text-left': align == 'Left' or is_featured,
}) -%} }) -%}
<div class="card-body {{ align_class }}" style="width:100%"> <div class="card-body {{ align_class }}" style="width:100%">
<div style="margin-top: 16px; display: flex;"> <div class="mt-4">
<a href="/{{ item.route or '#' }}"> <a href="/{{ item.route or '#' }}">
<div class="product-title"> <div class="product-title">
{{ title or '' }} {{ title or '' }}
{% if item.in_stock %}
<span class="indicator {{ item.in_stock }} card-indicator"></span>
{% endif %}
</div> </div>
</a> </a>
</div> </div>
{% if is_featured %} {% if is_featured %}
<div class="product-price">{{ item.formatted_price or '' }}</div> <div class="product-description ellipsis text-muted" style="white-space: normal;">
<div class="product-description ellipsis">{{ description or '' }}</div> {{ description or '' }}
</div>
{% else %} {% else %}
<div class="product-category">{{ item.item_group or '' }}</div> <div class="product-category">{{ item.item_group or '' }}</div>
{% endif %} {% endif %}

View File

@@ -1,15 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
no_cache = 1
import frappe import frappe
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
def get_context(context): def get_context(context):
context.no_cache = 1
context.full_page = True context.full_page = True
context.reviews = None context.reviews = None
if frappe.form_dict and frappe.form_dict.get("item_code"): if frappe.form_dict and frappe.form_dict.get("item_code"):
context.item_code = frappe.form_dict.get("item_code") context.item_code = frappe.form_dict.get("item_code")
context.web_item = frappe.db.get_value("Website Item", {"item_code": context.item_code}, "name") context.web_item = frappe.db.get_value("Website Item", {"item_code": context.item_code}, "name")

View File

@@ -1,9 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
no_cache = 1
import frappe import frappe
from erpnext.utilities.product import get_price from erpnext.utilities.product import get_price
from erpnext.e_commerce.shopping_cart.cart import _set_price_list from erpnext.e_commerce.shopping_cart.cart import _set_price_list
@@ -32,6 +28,7 @@ def get_context(context):
context.items = items context.items = items
context.settings = settings context.settings = settings
context.no_cache = 1
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):
stock_qty = frappe.utils.flt( stock_qty = frappe.utils.flt(
@@ -42,7 +39,7 @@ def get_stock_availability(item_code, warehouse):
}, },
"actual_qty") "actual_qty")
) )
return True if stock_qty else False return bool(stock_qty)
def get_wishlist_items(): def get_wishlist_items():
if frappe.db.exists("Wishlist", frappe.session.user): if frappe.db.exists("Wishlist", frappe.session.user):
@@ -53,5 +50,5 @@ def get_wishlist_items():
from from
`tabWishlist Items` `tabWishlist Items`
where where
parent=%(user)s""" % {"user": frappe.db.escape(frappe.session.user)}, as_dict=1) parent=%(user)s""", {"user": frappe.session.user}, as_dict=1)
return return

View File

@@ -5,86 +5,18 @@ $(() => {
let is_item_group_page = $(".item-group-content").data("item-group"); let is_item_group_page = $(".item-group-content").data("item-group");
this.item_group = is_item_group_page || null; this.item_group = is_item_group_page || null;
// Render Products and Discount Filters let view_type = "List View";
// Render Product Views and setup Filters
frappe.require('assets/js/e-commerce.min.js', function() { frappe.require('assets/js/e-commerce.min.js', function() {
new erpnext.ProductView({ new erpnext.ProductView({
view_type: "List", view_type: view_type,
products_section: $('#product-listing'), products_section: $('#product-listing'),
item_group: me.item_group item_group: me.item_group
}); });
}); });
this.bind_filters();
this.bind_card_actions(); this.bind_card_actions();
this.bind_search();
this.restore_filters_state();
}
bind_filters() {
let me = this;
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') || $checkbox.is('.discount-filter')) {
const {
filterName: filter_name,
filterValue: filter_value
} = $checkbox.data();
if ($checkbox.is('.discount-filter')) {
// clear previous discount filter to accomodate new
delete this.field_filters["discount"];
}
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', '', `${location.pathname}?` + query_string);
$('.page_content input').prop('disabled', true);
frappe.require('assets/js/e-commerce.min.js', function() {
new erpnext.ProductView({
view_type: "List",
products_section: $('#product-listing'),
item_group: me.item_group
});
$('.page_content input').prop('disabled', false);
});
}, 1000));
} }
bind_card_actions() { bind_card_actions() {
@@ -92,70 +24,20 @@ $(() => {
e_commerce.wishlist.bind_wishlist_action(); e_commerce.wishlist.bind_wishlist_action();
} }
bind_search() { // bind_search() {
$('input[type=search]').on('keydown', (e) => { // $('input[type=search]').on('keydown', (e) => {
if (e.keyCode === 13) { // if (e.keyCode === 13) {
// Enter // // Enter
const value = e.target.value; // const value = e.target.value;
if (value) { // if (value) {
window.location.search = 'search=' + e.target.value; // window.location.search = 'search=' + e.target.value;
} else { // } else {
window.location.search = ''; // 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;
}
}
} }
new ProductListing(); 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;
}
}); });

View File

@@ -11,18 +11,6 @@
width: 300px !important; width: 300px !important;
margin: 30px !important; margin: 30px !important;
} }
.placeholder-div {
height:80%;
width: -webkit-fill-available;
padding: 50px;
text-align: center;
background-color: #F9FAFA;
border-top-left-radius: calc(0.75rem - 1px);
border-top-right-radius: calc(0.75rem - 1px);
}
.placeholder {
font-size: 72px;
}
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -4,7 +4,7 @@ from frappe import _
sitemap = 1 sitemap = 1
def get_context(context): def get_context(context):
settings = frappe.get_doc("E Commerce Settings") settings = frappe.get_cached_doc("E Commerce Settings")
context.categories_enabled = settings.enable_field_filters context.categories_enabled = settings.enable_field_filters
if context.categories_enabled: if context.categories_enabled:
@@ -23,9 +23,9 @@ def get_slideshow(slideshow):
'rounded': 1, 'rounded': 1,
'slider_name': "Categories" 'slider_name': "Categories"
} }
slideshow = frappe.get_doc("Website Slideshow", slideshow) slideshow = frappe.get_cached_doc("Website Slideshow", slideshow)
slides = slideshow.get({"doctype": "Website Slideshow Item"}) slides = slideshow.get({"doctype": "Website Slideshow Item"})
for index, slide in enumerate(slides): for index, slide in enumerate(slides, start=1):
values[f"slide_{index + 1}_image"] = slide.image values[f"slide_{index + 1}_image"] = slide.image
values[f"slide_{index + 1}_title"] = slide.heading values[f"slide_{index + 1}_title"] = slide.heading
values[f"slide_{index + 1}_subtitle"] = slide.description values[f"slide_{index + 1}_subtitle"] = slide.description
@@ -41,7 +41,7 @@ def get_tabs(categories):
} }
categorical_data = get_category_records(categories) categorical_data = get_category_records(categories)
for index, tab in enumerate(categorical_data): for index, tab in enumerate(categorical_data, start=1):
tab_values[f"tab_{index + 1}_title"] = frappe.unscrub(tab) tab_values[f"tab_{index + 1}_title"] = frappe.unscrub(tab)
# pre-render cards for each tab # pre-render cards for each tab
tab_values[f"tab_{index + 1}_content"] = frappe.render_template( tab_values[f"tab_{index + 1}_content"] = frappe.render_template(
@@ -55,19 +55,24 @@ def get_category_records(categories):
for category in categories: for category in categories:
if category == "item_group": if category == "item_group":
categorical_data["item_group"] = frappe.db.sql(""" categorical_data["item_group"] = frappe.db.sql("""
Select name, parent_item_group, is_group, image, route Select
from `tabItem Group` name, parent_item_group, is_group, image, route
where parent_item_group='All Item Groups' from
and show_in_website=1""", as_dict=1) `tabItem Group`
where
parent_item_group = 'All Item Groups'
and show_in_website = 1
""",
as_dict=1)
else: else:
doctype = frappe.unscrub(category) doctype = frappe.unscrub(category)
fields = ["name"] fields = ["name"]
if frappe.get_meta(doctype, cached=True).get_field("image"): if frappe.get_meta(doctype, cached=True).get_field("image"):
fields += ["image"] fields += ["image"]
categorical_data[category] = frappe.db.sql(""" categorical_data[category] = frappe.db.sql(f"""
Select {fields} Select {",".join(fields)}
from `tab{doctype}`""".format(doctype=doctype, fields=",".join(fields)), as_dict=1) from `tab{doctype}`""", as_dict=1)
return categorical_data return categorical_data