@@ -24,6 +24,7 @@ install: | |||
- cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ | |||
script: | |||
- set -e | |||
- bench --verbose run-tests | |||
- bench reinstall --yes | |||
- testcafe chrome apps/frappe/frappe/tests/testcafe/ | |||
@@ -13,7 +13,7 @@ import os, sys, importlib, inspect, json | |||
from .exceptions import * | |||
from .utils.jinja import get_jenv, get_template, render_template | |||
__version__ = '8.0.43' | |||
__version__ = '8.0.44' | |||
__title__ = "Frappe Framework" | |||
local = Local() | |||
@@ -7,6 +7,11 @@ def get_data(): | |||
"label": _("Payments"), | |||
"icon": "fa fa-star", | |||
"items": [ | |||
{ | |||
"type": "doctype", | |||
"name": "Stripe Settings", | |||
"description": _("Stripe payment gateway settings"), | |||
}, | |||
{ | |||
"type": "doctype", | |||
"name": "PayPal Settings", | |||
@@ -902,7 +902,7 @@ | |||
"issingle": 1, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-05-01 15:27:11.079447", | |||
"modified": "2017-05-11 15:27:11.079447", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "System Settings", | |||
@@ -13,7 +13,9 @@ frappe.ui.form.on('Custom Field', { | |||
if(user!=="Administrator") { | |||
filters.push(['DocType', 'module', '!=', 'Core']) | |||
} | |||
return filters | |||
return { | |||
"filters": filters | |||
} | |||
}); | |||
}, | |||
refresh: function(frm) { | |||
@@ -118,10 +118,13 @@ def update_order(board_name, order): | |||
def quick_kanban_board(doctype, board_name, field_name): | |||
'''Create new KanbanBoard quickly with default options''' | |||
doc = frappe.new_doc('Kanban Board') | |||
options = frappe.get_value('DocField', dict( | |||
parent=doctype, | |||
fieldname=field_name | |||
), 'options') | |||
meta = frappe.get_meta(doctype) | |||
options = '' | |||
for field in meta.fields: | |||
if field.fieldname == field_name: | |||
options = field.options | |||
columns = [] | |||
if options: | |||
@@ -198,4 +201,4 @@ def set_indicator(board_name, column_name, indicator): | |||
def save_filters(board_name, filters): | |||
'''Save filters silently''' | |||
frappe.db.set_value('Kanban Board', board_name, 'filters', | |||
filters, update_modified=False) | |||
filters, update_modified=False) |
@@ -3,7 +3,7 @@ | |||
# Search | |||
from __future__ import unicode_literals | |||
import frappe | |||
import frappe, json | |||
from frappe.utils import cstr, unique | |||
# this is called by the Link Field | |||
@@ -16,7 +16,7 @@ def search_link(doctype, txt, query=None, filters=None, page_len=20, searchfield | |||
# this is called by the search box | |||
@frappe.whitelist() | |||
def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||
page_len=10, filters=None, as_dict=False): | |||
page_len=10, filters=None, filter_fields=None, as_dict=False): | |||
if isinstance(filters, basestring): | |||
import json | |||
filters = json.loads(filters) | |||
@@ -76,20 +76,24 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||
if meta.get("fields", {"fieldname":"disabled", "fieldtype":"Check"}): | |||
filters.append([doctype, "disabled", "!=", 1]) | |||
# format a list of fields combining search fields and filter fields | |||
fields = get_std_fields_list(meta, searchfield or "name") | |||
if filter_fields: | |||
fields = list(set(fields + json.loads(filter_fields))) | |||
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields] | |||
# find relevance as location of search term from the beginning of string `name`. used for sorting results. | |||
fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format( | |||
formatted_fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format( | |||
_txt=frappe.db.escape((txt or "").replace("%", "")), doctype=frappe.db.escape(doctype))) | |||
# In order_by, `idx` gets second priority, because it stores link count | |||
from frappe.model.db_query import get_order_by | |||
order_by_based_on_meta = get_order_by(doctype, meta) | |||
order_by = "if(_relevance, _relevance, 99999), idx desc, {0}".format(order_by_based_on_meta) | |||
values = frappe.get_list(doctype, | |||
filters=filters, fields=fields, | |||
filters=filters, fields=formatted_fields, | |||
or_filters = or_filters, limit_start = start, | |||
limit_page_length=page_len, | |||
order_by=order_by, | |||
@@ -97,6 +101,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||
as_list=not as_dict) | |||
# remove _relevance from results | |||
frappe.response["fields"] = fields | |||
frappe.response["values"] = [r[:-1] for r in values] | |||
def get_std_fields_list(meta, key): | |||
@@ -107,7 +112,7 @@ def get_std_fields_list(meta, key): | |||
if not key in sflist: | |||
sflist = sflist + [key] | |||
return ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in sflist] | |||
return sflist | |||
def build_for_autosuggest(res): | |||
results = [] | |||
@@ -217,7 +217,14 @@ def evaluate_alert(doc, alert, event): | |||
return | |||
if event=="Value Change" and not doc.is_new(): | |||
db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) | |||
try: | |||
db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) | |||
except frappe.DatabaseOperationalError as e: | |||
if e.args[0]==1054: | |||
alert.db_set('enabled', 0) | |||
frappe.log_error('Email Alert {0} has been disabled due to missing field'.format(alert.name)) | |||
return | |||
db_value = parse_val(db_value) | |||
if (doc.get(alert.value_changed) == db_value) or \ | |||
(not db_value and not doc.get(alert.value_changed)): | |||
@@ -87,6 +87,36 @@ class TestEmailAlert(unittest.TestCase): | |||
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", | |||
"reference_name": event.name, "status":"Not Sent"})) | |||
def test_alert_disabled_on_wrong_field(self): | |||
frappe.set_user('Administrator') | |||
email_alert = frappe.get_doc({ | |||
"doctype": "Email Alert", | |||
"subject":"_Test Email Alert for wrong field", | |||
"document_type": "Event", | |||
"event": "Value Change", | |||
"attach_print": 0, | |||
"value_changed": "description1", | |||
"message": "Description changed", | |||
"recipients": [ | |||
{ "email_by_document_field": "owner" } | |||
] | |||
}).insert() | |||
event = frappe.new_doc("Event") | |||
event.subject = "test-2", | |||
event.event_type = "Private" | |||
event.starts_on = "2014-06-06 12:00:00" | |||
event.insert() | |||
event.subject = "test 1" | |||
event.save() | |||
# verify that email_alert is disabled | |||
email_alert.reload() | |||
self.assertEqual(email_alert.enabled, 0) | |||
email_alert.delete() | |||
event.delete() | |||
def test_date_changed(self): | |||
event = frappe.new_doc("Event") | |||
event.subject = "test", | |||
@@ -7,6 +7,7 @@ from __future__ import unicode_literals | |||
from werkzeug.exceptions import NotFound | |||
from MySQLdb import ProgrammingError as SQLError, Error | |||
from MySQLdb import OperationalError as DatabaseOperationalError | |||
class ValidationError(Exception): | |||
@@ -25,16 +25,21 @@ def make_mapped_doc(method, source_name, selected_children=None): | |||
return method(source_name) | |||
@frappe.whitelist() | |||
def map_docs(method, source_names, target_doc): | |||
'''Returns the mapped document calling the given mapper method | |||
with each of the given source docs on the target doc''' | |||
method = frappe.get_attr(method) | |||
if method not in frappe.whitelisted: | |||
raise frappe.PermissionError | |||
for src in json.loads(source_names): | |||
target_doc = method(src, target_doc) | |||
return target_doc | |||
def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, | |||
postprocess=None, ignore_permissions=False, ignore_child_tables=False): | |||
source_doc = frappe.get_doc(from_doctype, from_docname) | |||
if not ignore_permissions: | |||
if not source_doc.has_permission("read"): | |||
source_doc.raise_no_permission_to("read") | |||
# main | |||
if not target_doc: | |||
target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"]) | |||
@@ -44,6 +49,12 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, | |||
if not ignore_permissions and not target_doc.has_permission("create"): | |||
target_doc.raise_no_permission_to("create") | |||
source_doc = frappe.get_doc(from_doctype, from_docname) | |||
if not ignore_permissions: | |||
if not source_doc.has_permission("read"): | |||
source_doc.raise_no_permission_to("read") | |||
map_doc(source_doc, target_doc, table_maps[source_doc.doctype]) | |||
row_exists_for_parentfield = {} | |||
@@ -94,12 +94,15 @@ def make_autoname(key='', doctype='', doc=''): | |||
elif not "." in key: | |||
frappe.throw(_("Invalid naming series (. missing)") + (_(" for {0}").format(doctype) if doctype else "")) | |||
parts = key.split('.') | |||
n = parse_naming_series(parts, doctype, doc) | |||
return n | |||
def parse_naming_series(parts, doctype= '', doc = ''): | |||
n = '' | |||
l = key.split('.') | |||
series_set = False | |||
today = now_datetime() | |||
for e in l: | |||
for e in parts: | |||
part = '' | |||
if e.startswith('#'): | |||
if not series_set: | |||
@@ -120,6 +123,7 @@ def make_autoname(key='', doctype='', doc=''): | |||
if isinstance(part, basestring): | |||
n+=part | |||
return n | |||
def getseries(key, digits, doctype=''): | |||
@@ -12,7 +12,7 @@ frappe.patches.v8_0.drop_in_dialog | |||
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2017-03-03 | |||
execute:frappe.reload_doc('core', 'doctype', 'docperm') #2017-03-03 | |||
frappe.patches.v8_0.drop_is_custom_from_docperm | |||
frappe.patches.v8_0.update_records_in_global_search | |||
frappe.patches.v8_0.update_records_in_global_search #11-05-2017 | |||
frappe.patches.v8_0.update_published_in_global_search | |||
execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') | |||
execute:frappe.reload_doc('core', 'doctype', 'deleted_document') | |||
@@ -1,5 +1,7 @@ | |||
import frappe | |||
from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype | |||
def execute(): | |||
frappe.cache().delete_value('doctypes_with_global_search') | |||
for doctype in get_doctypes_with_global_search(with_child_tables=False): | |||
rebuild_for_doctype(doctype) |
@@ -330,7 +330,8 @@ def get_all_perms(role): | |||
'''Returns valid permissions for a given role''' | |||
perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role)) | |||
custom_perms = frappe.get_all('Custom DocPerm', fields='*', filters=dict(role=role)) | |||
doctypes_with_custom_perms = list(set(p.parent for p in custom_perms)) | |||
doctypes_with_custom_perms = frappe.db.sql_list("""select distinct parent | |||
from `tabCustom DocPerm`""") | |||
for p in perms: | |||
if p.parent not in doctypes_with_custom_perms: | |||
@@ -30,6 +30,7 @@ | |||
"public/js/frappe/ui/field_group.js", | |||
"public/js/frappe/form/control.js", | |||
"public/js/frappe/form/link_selector.js", | |||
"public/js/frappe/form/multi_select_dialog.js", | |||
"public/js/frappe/ui/dialog.js" | |||
], | |||
"css/desk.min.css": [ | |||
@@ -104,6 +105,7 @@ | |||
"public/js/frappe/ui/field_group.js", | |||
"public/js/frappe/form/control.js", | |||
"public/js/frappe/form/link_selector.js", | |||
"public/js/frappe/form/multi_select_dialog.js", | |||
"public/js/frappe/ui/dialog.js", | |||
"public/js/frappe/ui/app_icon.js", | |||
@@ -978,6 +978,13 @@ input[type="checkbox"]:checked:before { | |||
font-size: 13px; | |||
color: #3b99fc; | |||
} | |||
.multiselect-empty-state { | |||
min-height: 300px; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
height: 100%; | |||
} | |||
@-moz-document url-prefix() { | |||
input[type="checkbox"] { | |||
visibility: visible; | |||
@@ -447,8 +447,9 @@ frappe.ui.form.Timeline = Class.extend({ | |||
var parts = [], count = 0; | |||
data.row_changed.every(function(row) { | |||
row[3].every(function(p) { | |||
var df = frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, | |||
p[0], me.frm.docname); | |||
var df = me.frm.fields_dict[row[0]] && | |||
frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, | |||
p[0], me.frm.docname); | |||
if(df && !df.hidden) { | |||
field_display_status = frappe.perm.get_field_display_status(df, | |||
@@ -0,0 +1,212 @@ | |||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
// MIT License. See license.txt | |||
frappe.ui.form.MultiSelectDialog = Class.extend({ | |||
init: function(opts) { | |||
/* Options: doctype, target, setters, get_query, action */ | |||
$.extend(this, opts); | |||
var me = this; | |||
if(this.doctype!="[Select]") { | |||
frappe.model.with_doctype(this.doctype, function(r) { | |||
me.make(); | |||
}); | |||
} else { | |||
this.make(); | |||
} | |||
}, | |||
make: function() { | |||
let me = this; | |||
let fields = []; | |||
let count = 0; | |||
if(!this.date_field) { | |||
this.date_field = "transaction_date"; | |||
} | |||
Object.keys(this.setters).forEach(function(setter) { | |||
fields.push({ | |||
fieldtype: me.target.fields_dict[setter].df.fieldtype, | |||
label: me.target.fields_dict[setter].df.label, | |||
fieldname: setter, | |||
options: me.target.fields_dict[setter].df.options, | |||
default: me.setters[setter] | |||
}); | |||
if (count++ < Object.keys(me.setters).length - 1) { | |||
fields.push({fieldtype: "Column Break"}); | |||
} | |||
}); | |||
fields = fields.concat([ | |||
{ fieldtype: "Section Break" }, | |||
{ fieldtype: "HTML", fieldname: "results_area" }, | |||
{ fieldtype: "Button", fieldname: "make_new", label: __("Make a new " + me.doctype) } | |||
]); | |||
let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's' | |||
: this.doctype.slice(0, -1) + 'ies'; | |||
this.dialog = new frappe.ui.Dialog({ | |||
title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]), | |||
fields: fields, | |||
primary_action_label: __("Get Items"), | |||
primary_action: function() { | |||
me.action(me.get_checked_values(), me.args); | |||
} | |||
}); | |||
this.$parent = $(this.dialog.body); | |||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results" | |||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`); | |||
this.$results = this.$wrapper.find('.results'); | |||
this.$make_new_btn = this.dialog.fields_dict.make_new.$wrapper; | |||
this.$placeholder = $(`<div class="multiselect-empty-state"> | |||
<span class="text-center" style="margin-top: -40px;"> | |||
<i class="fa fa-2x fa-tags text-extra-muted"></i> | |||
<p class="text-extra-muted">No ${this.doctype} found</p> | |||
<button class="btn btn-default btn-xs text-muted" data-fieldtype="Button" | |||
data-fieldname="make_new" placeholder="" value="">Make a new ${this.doctype}</button> | |||
</span> | |||
</div>`); | |||
this.args = {}; | |||
this.bind_events(); | |||
this.get_results(); | |||
this.dialog.show(); | |||
}, | |||
bind_events: function() { | |||
let me = this; | |||
this.$results.on('click', '.list-item-container', function (e) { | |||
if (!$(e.target).is(':checkbox') && !$(e.target).is('a')) { | |||
$(this).find(':checkbox').trigger('click'); | |||
} | |||
}); | |||
this.$results.on('click', '.list-item--head :checkbox', (e) => { | |||
this.$results.find('.list-item-container .list-row-check') | |||
.prop("checked", ($(e.target).is(':checked'))); | |||
}); | |||
this.$parent.find('.input-with-feedback').on('change', (e) => { | |||
this.get_results(); | |||
}); | |||
this.$parent.on('click', '.btn[data-fieldname="make_new"]', (e) => { | |||
frappe.route_options = {}; | |||
Object.keys(this.setters).forEach(function(setter) { | |||
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined; | |||
}); | |||
frappe.new_doc(this.doctype, true); | |||
}); | |||
}, | |||
get_checked_values: function() { | |||
return this.$results.find('.list-item-container').map(function() { | |||
if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) { | |||
return $(this).attr('data-item-name'); | |||
} | |||
}).get(); | |||
}, | |||
make_list_row: function(result={}) { | |||
var me = this; | |||
// Make a head row by default (if result not passed) | |||
let head = Object.keys(result).length === 0; | |||
let contents = ``; | |||
let columns = (["name"].concat(Object.keys(this.setters))).concat("Date"); | |||
columns.forEach(function(column) { | |||
contents += `<div class="list-item__content ellipsis"> | |||
${ | |||
head ? __(frappe.model.unscrub(column)) | |||
: (column !== "name" ? __(result[column]) | |||
: `<a href="${"#Form/"+ me.doctype + "/" + result[column]}" class="list-id"> | |||
${__(result[column])}</a>`) | |||
} | |||
</div>`; | |||
}) | |||
let $row = $(`<div class="list-item"> | |||
<div class="list-item__content ellipsis" style="flex: 0 0 10px;"> | |||
<input type="checkbox" class="list-row-check" ${result.checked ? 'checked' : ''}> | |||
</div> | |||
${contents} | |||
</div>`); | |||
head ? $row.addClass('list-item--head') | |||
: $row = $(`<div class="list-item-container" data-item-name="${result.name}"></div>`).append($row); | |||
return $row; | |||
}, | |||
render_result_list: function(results) { | |||
var me = this; | |||
this.$results.empty(); | |||
if(results.length === 0) { | |||
this.$make_new_btn.addClass('hide'); | |||
this.$results.append(me.$placeholder); | |||
return; | |||
} | |||
this.$make_new_btn.removeClass('hide'); | |||
this.$results.append(this.make_list_row()); | |||
results.forEach((result) => { | |||
me.$results.append(me.make_list_row(result)); | |||
}) | |||
}, | |||
get_results: function() { | |||
let me = this; | |||
let filters = this.get_query().filters; | |||
Object.keys(this.setters).forEach(function(setter) { | |||
filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined; | |||
me.args[setter] = filters[setter]; | |||
}); | |||
let args = { | |||
doctype: me.doctype, | |||
txt: '', | |||
filters: filters, | |||
filter_fields: Object.keys(me.setters).concat([me.date_field]) | |||
} | |||
frappe.call({ | |||
type: "GET", | |||
method:'frappe.desk.search.search_widget', | |||
no_spinner: true, | |||
args: args, | |||
callback: function(r) { | |||
if(r.values) { | |||
let results = []; | |||
r.values.forEach(function(value_list) { | |||
let result = {}; | |||
value_list.forEach(function(value, index){ | |||
if(r.fields[index] === me.date_field) { | |||
result["Date"] = value; | |||
} else { | |||
result[r.fields[index]] = value; | |||
} | |||
}); | |||
result.checked = 0; | |||
result.parsed_date = Date.parse(result["Date"]); | |||
results.push(result); | |||
}); | |||
results.map( (result) => { | |||
result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"}); | |||
}) | |||
results.sort((a, b) => { | |||
return a.parsed_date - b.parsed_date; | |||
}); | |||
// Preselect oldest entry | |||
results[0].checked = 1 | |||
me.render_result_list(results); | |||
} | |||
} | |||
}); | |||
}, | |||
}); |
@@ -31,7 +31,7 @@ frappe.ui.form.quick_entry = function(doctype, success) { | |||
} | |||
var dialog = new frappe.ui.Dialog({ | |||
title: __("New {0}", [doctype]), | |||
title: __("New {0}", [__(doctype)]), | |||
fields: mandatory, | |||
}); | |||
@@ -144,7 +144,7 @@ frappe.views.ListRenderer = Class.extend({ | |||
} | |||
// kanban column fields | |||
if (me.meta.__kanban_column_fields) { | |||
me.fields = me.fields.concat(me.meta.__kanban_column_fields); | |||
me.meta.__kanban_column_fields.map(add_field); | |||
} | |||
}, | |||
set_columns: function () { | |||
@@ -187,6 +187,11 @@ $.extend(frappe.model, { | |||
return txt.replace(/ /g, "_").toLowerCase(); | |||
}, | |||
unscrub: function(txt) { | |||
return __(txt || '').replace(/-|_/g, " ").replace(/\w*/g, | |||
function(keywords){return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();}); | |||
}, | |||
can_create: function(doctype) { | |||
return frappe.boot.user.can_create.indexOf(doctype)!==-1; | |||
}, | |||
@@ -67,7 +67,6 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ | |||
var f = this.fields_dict[key]; | |||
if(f.get_parsed_value) { | |||
var v = f.get_parsed_value(); | |||
if(f.df.reqd && is_null(v)) | |||
errors.push(__(f.df.label)); | |||
@@ -8,7 +8,6 @@ frappe.ui.keys.setup = function() { | |||
for(var i=0, l = frappe.ui.keys.handlers[key].length; i<l; i++) { | |||
var handler = frappe.ui.keys.handlers[key][i]; | |||
var _out = handler.apply(this, [e]); | |||
if(_out===false) { | |||
out = _out; | |||
} | |||
@@ -23,10 +22,9 @@ frappe.ui.keys.get_key = function(e) { | |||
//safari doesn't have key property | |||
if(!key) { | |||
var keycode = e.keyCode || e.which; | |||
key = frappe.ui.keys.key_map[keycode] || | |||
String.fromCharCode(keycode); | |||
key = frappe.ui.keys.key_map[keycode] || String.fromCharCode(keycode); | |||
} | |||
if(key.substr(0, 5)==='Arrow') { | |||
if(key.substr(0, 5) === 'Arrow') { | |||
// ArrowDown -> down | |||
key = key.substr(5).toLowerCase(); | |||
} | |||
@@ -69,22 +67,13 @@ frappe.ui.keys.on('ctrl+b', function(e) { | |||
} | |||
}); | |||
frappe.ui.keys.on('Escape', function(e) { | |||
// close open grid row | |||
var open_row = $(".grid-row-open"); | |||
if(open_row.length) { | |||
var grid_row = open_row.data("grid_row"); | |||
grid_row.toggle_view(false); | |||
return false; | |||
} | |||
// close open dialog | |||
if(cur_dialog && !cur_dialog.no_cancel_flag) { | |||
cur_dialog.cancel(); | |||
return false; | |||
} | |||
frappe.ui.keys.on('escape', function(e) { | |||
close_grid_and_dialog(); | |||
}); | |||
frappe.ui.keys.on('esc', function(e) { | |||
close_grid_and_dialog(); | |||
}); | |||
frappe.ui.keys.on('Enter', function(e) { | |||
if(cur_dialog && cur_dialog.confirm_dialog) { | |||
@@ -127,4 +116,20 @@ frappe.ui.keyCode = { | |||
ENTER: 13, | |||
TAB: 9, | |||
SPACE: 32 | |||
} | |||
function close_grid_and_dialog() { | |||
// close open grid row | |||
var open_row = $(".grid-row-open"); | |||
if (open_row.length) { | |||
var grid_row = open_row.data("grid_row"); | |||
grid_row.toggle_view(false); | |||
return false; | |||
} | |||
// close open dialog | |||
if (cur_dialog && !cur_dialog.no_cancel_flag) { | |||
cur_dialog.cancel(); | |||
return false; | |||
} | |||
} |
@@ -91,16 +91,12 @@ frappe.ui.SortSelector = Class.extend({ | |||
var me = this; | |||
var meta = frappe.get_meta(this.doctype); | |||
var { meta_sort_field, meta_sort_order } = this.get_meta_sort_field(); | |||
if(!this.args.sort_by) { | |||
if(meta.sort_field) { | |||
if(meta.sort_field.indexOf(',')!==-1) { | |||
parts = meta.sort_field.split(',')[0].split(' '); | |||
this.args.sort_by = parts[0]; | |||
this.args.sort_order = parts[1]; | |||
} else { | |||
this.args.sort_by = meta.sort_field; | |||
this.args.sort_order = meta.sort_order.toLowerCase(); | |||
} | |||
if(meta_sort_field) { | |||
this.args.sort_by = meta_sort_field; | |||
this.args.sort_order = meta_sort_order; | |||
} else { | |||
// default | |||
this.args.sort_by = 'modified'; | |||
@@ -115,7 +111,7 @@ frappe.ui.SortSelector = Class.extend({ | |||
if(!this.args.options) { | |||
// default options | |||
var _options = [ | |||
{'fieldname': 'modified'}, | |||
{'fieldname': 'modified'} | |||
] | |||
// title field | |||
@@ -130,9 +126,15 @@ frappe.ui.SortSelector = Class.extend({ | |||
} | |||
}); | |||
_options.push({'fieldname': 'name'}); | |||
_options.push({'fieldname': 'creation'}); | |||
_options.push({'fieldname': 'idx'}); | |||
// meta sort field | |||
if(meta_sort_field) _options.push({ 'fieldname': meta_sort_field }); | |||
// more default options | |||
_options.push( | |||
{'fieldname': 'name'}, | |||
{'fieldname': 'creation'}, | |||
{'fieldname': 'idx'} | |||
) | |||
// de-duplicate | |||
this.args.options = _options.uniqBy(function(obj) { | |||
@@ -151,6 +153,21 @@ frappe.ui.SortSelector = Class.extend({ | |||
this.sort_by = this.args.sort_by; | |||
this.sort_order = this.args.sort_order; | |||
}, | |||
get_meta_sort_field: function() { | |||
var meta = frappe.get_meta(this.doctype); | |||
if(meta.sort_field && meta.sort_field.includes(',')) { | |||
var parts = meta.sort_field.split(',')[0].split(' '); | |||
return { | |||
meta_sort_field: parts[0], | |||
meta_sort_order: parts[1] | |||
} | |||
} else { | |||
return { | |||
meta_sort_field: meta.sort_field, | |||
meta_sort_order: meta.sort_order.toLowerCase() | |||
} | |||
} | |||
}, | |||
get_label: function(fieldname) { | |||
if(fieldname==='idx') { | |||
return __("Most Used"); | |||
@@ -575,10 +575,6 @@ frappe.search.utils = { | |||
return rendered; | |||
} | |||
}, | |||
} | |||
unscrub_and_titlecase: function(str) { | |||
return __(str || '').replace(/-|_/g, " ").replace(/\w*/g, | |||
function(keywords){return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();}); | |||
}, | |||
} |
@@ -41,6 +41,12 @@ frappe.provide("frappe.views"); | |||
columns: columns, | |||
cur_list: opts.cur_list | |||
}); | |||
}) | |||
.fail(function() { | |||
// redirect back to List | |||
setTimeout(() => { | |||
frappe.set_route('List', opts.doctype, 'List'); | |||
}, 2000); | |||
}); | |||
}, | |||
update_cards: function (updater, cards) { | |||
@@ -1038,6 +1044,9 @@ frappe.provide("frappe.views"); | |||
function is_filters_modified(board, cur_list) { | |||
return new Promise(function(resolve, reject) { | |||
setTimeout(function() { | |||
// sometimes the filter_list is not initiated, so early return | |||
if(!cur_list.filter_list) resolve(false); | |||
var list_filters = JSON.stringify(cur_list.filter_list.get_filters()); | |||
resolve(list_filters !== board.filters); | |||
}, 2000); | |||
@@ -227,7 +227,7 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({ | |||
set_route_filters: function(first_load) { | |||
var me = this; | |||
if(frappe.route_options && !this.user_settings.filters) { | |||
if(frappe.route_options) { | |||
this.set_filters_from_route_options(); | |||
return true; | |||
} else if(this.user_settings | |||
@@ -548,9 +548,12 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({ | |||
$.each(frappe.model.get_all_docs(doc), function(i, d) { | |||
// find the document of the current updated record | |||
// from locals (which is synced in the response) | |||
if(item[d.doctype + ":name"]===d.name) { | |||
for(k in d) { | |||
v = d[k]; | |||
var name = item[d.doctype + ":name"]; | |||
if(!name) name = item.name; | |||
if(name===d.name) { | |||
for(var k in d) { | |||
var v = d[k]; | |||
if(frappe.model.std_fields_list.indexOf(k)===-1 | |||
&& item[k]!==undefined) { | |||
new_item[k] = v; | |||
@@ -431,7 +431,7 @@ textarea.form-control { | |||
flex: 0 0 36px; | |||
order: -1; | |||
justify-content: flex-end; | |||
input[type="checkbox"] { | |||
margin-right: 0; | |||
} | |||
@@ -894,8 +894,17 @@ input[type="checkbox"] { | |||
} | |||
} | |||
// Will not be required after commonifying lists with empty state | |||
.multiselect-empty-state{ | |||
min-height: 300px; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
height: 100%; | |||
} | |||
// mozilla doesn't support pseudo elements on checkbox | |||
// mozilla doesn't support | |||
// pseudo elements on checkbox | |||
@-moz-document url-prefix() { | |||
input[type="checkbox"] { | |||
visibility: visible; | |||
@@ -54,7 +54,7 @@ def rebuild_for_doctype(doctype): | |||
:param doctype: Doctype ''' | |||
def _get_filters(): | |||
filters = frappe._dict({ "docstatus": ["!=", 1] }) | |||
filters = frappe._dict({ "docstatus": ["!=", 2] }) | |||
if meta.has_field("enabled"): | |||
filters.enabled = 1 | |||
if meta.has_field("disabled"): | |||
@@ -105,7 +105,7 @@ def import_country_and_currency(): | |||
country = frappe._dict(data[name]) | |||
add_country_and_currency(name, country) | |||
print() | |||
# enable frequently used currencies | |||
for currency in ("INR", "USD", "GBP", "EUR", "AED", "AUD", "JPY", "CNY", "CHF"): | |||
@@ -9,7 +9,6 @@ import openpyxl | |||
from cStringIO import StringIO | |||
from openpyxl.styles import Font | |||
import html2text | |||
# return xlsx file object | |||
def make_xlsx(data, sheet_name): | |||
@@ -24,19 +23,33 @@ def make_xlsx(data, sheet_name): | |||
clean_row = [] | |||
for item in row: | |||
if isinstance(item, basestring): | |||
obj = html2text.HTML2Text() | |||
obj.ignore_links = True | |||
obj.body_width = 0 | |||
obj = obj.handle(unicode(item or "")) | |||
obj = obj.rsplit('\n', 1) | |||
value = obj[0] | |||
value = handle_html(item) | |||
else: | |||
value = item | |||
clean_row.append(value) | |||
ws.append(clean_row) | |||
xlsx_file = StringIO() | |||
wb.save(xlsx_file) | |||
return xlsx_file | |||
return xlsx_file | |||
def handle_html(data): | |||
# import html2text | |||
from html2text import unescape, HTML2Text | |||
h = HTML2Text() | |||
h.unicode_snob = True | |||
h = h.unescape(data or "") | |||
obj = HTML2Text() | |||
obj.ignore_links = True | |||
obj.body_width = 0 | |||
value = obj.handle(h) | |||
value = value.split('\n', 1) | |||
value = value[0].split('# ',1) | |||
if len(value) < 2: | |||
return value[0] | |||
else: | |||
return value[1] |