瀏覽代碼

Merge branch 'develop'

version-14
Nabin Hait 8 年之前
父節點
當前提交
20519ffde9
共有 32 個文件被更改,包括 435 次插入85 次删除
  1. +1
    -0
      .travis.yml
  2. +1
    -1
      frappe/__init__.py
  3. +5
    -0
      frappe/config/integrations.py
  4. +1
    -1
      frappe/core/doctype/system_settings/system_settings.json
  5. +3
    -1
      frappe/custom/doctype/custom_field/custom_field.js
  6. +8
    -5
      frappe/desk/doctype/kanban_board/kanban_board.py
  7. +12
    -7
      frappe/desk/search.py
  8. +8
    -1
      frappe/email/doctype/email_alert/email_alert.py
  9. +30
    -0
      frappe/email/doctype/email_alert/test_email_alert.py
  10. +1
    -0
      frappe/exceptions.py
  11. +17
    -6
      frappe/model/mapper.py
  12. +7
    -3
      frappe/model/naming.py
  13. +1
    -1
      frappe/patches.txt
  14. +2
    -0
      frappe/patches/v8_0/update_records_in_global_search.py
  15. +2
    -1
      frappe/permissions.py
  16. +2
    -0
      frappe/public/build.json
  17. +7
    -0
      frappe/public/css/desk.css
  18. +3
    -2
      frappe/public/js/frappe/form/footer/timeline.js
  19. +212
    -0
      frappe/public/js/frappe/form/multi_select_dialog.js
  20. +1
    -1
      frappe/public/js/frappe/form/quick_entry.js
  21. +1
    -1
      frappe/public/js/frappe/list/list_renderer.js
  22. +5
    -0
      frappe/public/js/frappe/model/model.js
  23. +0
    -1
      frappe/public/js/frappe/ui/field_group.js
  24. +23
    -18
      frappe/public/js/frappe/ui/keyboard.js
  25. +30
    -13
      frappe/public/js/frappe/ui/sort_selector.js
  26. +1
    -5
      frappe/public/js/frappe/ui/toolbar/search_utils.js
  27. +9
    -0
      frappe/public/js/frappe/views/kanban/kanban_board.js
  28. +7
    -4
      frappe/public/js/frappe/views/reports/reportview.js
  29. +11
    -2
      frappe/public/less/desk.less
  30. +1
    -1
      frappe/utils/global_search.py
  31. +1
    -1
      frappe/utils/install.py
  32. +22
    -9
      frappe/utils/xlsxutils.py

+ 1
- 0
.travis.yml 查看文件

@@ -24,6 +24,7 @@ install:
- cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/


script: script:
- set -e
- bench --verbose run-tests - bench --verbose run-tests
- bench reinstall --yes - bench reinstall --yes
- testcafe chrome apps/frappe/frappe/tests/testcafe/ - testcafe chrome apps/frappe/frappe/tests/testcafe/


+ 1
- 1
frappe/__init__.py 查看文件

@@ -13,7 +13,7 @@ import os, sys, importlib, inspect, json
from .exceptions import * from .exceptions import *
from .utils.jinja import get_jenv, get_template, render_template from .utils.jinja import get_jenv, get_template, render_template


__version__ = '8.0.43'
__version__ = '8.0.44'
__title__ = "Frappe Framework" __title__ = "Frappe Framework"


local = Local() local = Local()


+ 5
- 0
frappe/config/integrations.py 查看文件

@@ -7,6 +7,11 @@ def get_data():
"label": _("Payments"), "label": _("Payments"),
"icon": "fa fa-star", "icon": "fa fa-star",
"items": [ "items": [
{
"type": "doctype",
"name": "Stripe Settings",
"description": _("Stripe payment gateway settings"),
},
{ {
"type": "doctype", "type": "doctype",
"name": "PayPal Settings", "name": "PayPal Settings",


+ 1
- 1
frappe/core/doctype/system_settings/system_settings.json 查看文件

@@ -902,7 +902,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-05-01 15:27:11.079447",
"modified": "2017-05-11 15:27:11.079447",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "System Settings", "name": "System Settings",


+ 3
- 1
frappe/custom/doctype/custom_field/custom_field.js 查看文件

@@ -13,7 +13,9 @@ frappe.ui.form.on('Custom Field', {
if(user!=="Administrator") { if(user!=="Administrator") {
filters.push(['DocType', 'module', '!=', 'Core']) filters.push(['DocType', 'module', '!=', 'Core'])
} }
return filters
return {
"filters": filters
}
}); });
}, },
refresh: function(frm) { refresh: function(frm) {


+ 8
- 5
frappe/desk/doctype/kanban_board/kanban_board.py 查看文件

@@ -118,10 +118,13 @@ def update_order(board_name, order):
def quick_kanban_board(doctype, board_name, field_name): def quick_kanban_board(doctype, board_name, field_name):
'''Create new KanbanBoard quickly with default options''' '''Create new KanbanBoard quickly with default options'''
doc = frappe.new_doc('Kanban Board') 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 = [] columns = []
if options: if options:
@@ -198,4 +201,4 @@ def set_indicator(board_name, column_name, indicator):
def save_filters(board_name, filters): def save_filters(board_name, filters):
'''Save filters silently''' '''Save filters silently'''
frappe.db.set_value('Kanban Board', board_name, 'filters', frappe.db.set_value('Kanban Board', board_name, 'filters',
filters, update_modified=False)
filters, update_modified=False)

+ 12
- 7
frappe/desk/search.py 查看文件

@@ -3,7 +3,7 @@


# Search # Search
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
import frappe, json
from frappe.utils import cstr, unique from frappe.utils import cstr, unique


# this is called by the Link Field # 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 # this is called by the search box
@frappe.whitelist() @frappe.whitelist()
def search_widget(doctype, txt, query=None, searchfield=None, start=0, 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): if isinstance(filters, basestring):
import json import json
filters = json.loads(filters) 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"}): if meta.get("fields", {"fieldname":"disabled", "fieldtype":"Check"}):
filters.append([doctype, "disabled", "!=", 1]) 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") 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. # 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))) _txt=frappe.db.escape((txt or "").replace("%", "")), doctype=frappe.db.escape(doctype)))


# In order_by, `idx` gets second priority, because it stores link count # In order_by, `idx` gets second priority, because it stores link count
from frappe.model.db_query import get_order_by from frappe.model.db_query import get_order_by
order_by_based_on_meta = get_order_by(doctype, meta) 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) order_by = "if(_relevance, _relevance, 99999), idx desc, {0}".format(order_by_based_on_meta)
values = frappe.get_list(doctype, values = frappe.get_list(doctype,
filters=filters, fields=fields,
filters=filters, fields=formatted_fields,
or_filters = or_filters, limit_start = start, or_filters = or_filters, limit_start = start,
limit_page_length=page_len, limit_page_length=page_len,
order_by=order_by, order_by=order_by,
@@ -97,6 +101,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
as_list=not as_dict) as_list=not as_dict)


# remove _relevance from results # remove _relevance from results
frappe.response["fields"] = fields
frappe.response["values"] = [r[:-1] for r in values] frappe.response["values"] = [r[:-1] for r in values]


def get_std_fields_list(meta, key): def get_std_fields_list(meta, key):
@@ -107,7 +112,7 @@ def get_std_fields_list(meta, key):
if not key in sflist: if not key in sflist:
sflist = sflist + [key] sflist = sflist + [key]


return ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in sflist]
return sflist


def build_for_autosuggest(res): def build_for_autosuggest(res):
results = [] results = []


+ 8
- 1
frappe/email/doctype/email_alert/email_alert.py 查看文件

@@ -217,7 +217,14 @@ def evaluate_alert(doc, alert, event):
return return


if event=="Value Change" and not doc.is_new(): 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) db_value = parse_val(db_value)
if (doc.get(alert.value_changed) == db_value) or \ if (doc.get(alert.value_changed) == db_value) or \
(not db_value and not doc.get(alert.value_changed)): (not db_value and not doc.get(alert.value_changed)):


+ 30
- 0
frappe/email/doctype/email_alert/test_email_alert.py 查看文件

@@ -87,6 +87,36 @@ class TestEmailAlert(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"})) "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): def test_date_changed(self):
event = frappe.new_doc("Event") event = frappe.new_doc("Event")
event.subject = "test", event.subject = "test",


+ 1
- 0
frappe/exceptions.py 查看文件

@@ -7,6 +7,7 @@ from __future__ import unicode_literals


from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from MySQLdb import ProgrammingError as SQLError, Error from MySQLdb import ProgrammingError as SQLError, Error
from MySQLdb import OperationalError as DatabaseOperationalError




class ValidationError(Exception): class ValidationError(Exception):


+ 17
- 6
frappe/model/mapper.py 查看文件

@@ -25,16 +25,21 @@ def make_mapped_doc(method, source_name, selected_children=None):


return method(source_name) 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, def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None,
postprocess=None, ignore_permissions=False, ignore_child_tables=False): 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 # main
if not target_doc: if not target_doc:
target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"]) 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"): if not ignore_permissions and not target_doc.has_permission("create"):
target_doc.raise_no_permission_to("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]) map_doc(source_doc, target_doc, table_maps[source_doc.doctype])


row_exists_for_parentfield = {} row_exists_for_parentfield = {}


+ 7
- 3
frappe/model/naming.py 查看文件

@@ -94,12 +94,15 @@ def make_autoname(key='', doctype='', doc=''):
elif not "." in key: elif not "." in key:
frappe.throw(_("Invalid naming series (. missing)") + (_(" for {0}").format(doctype) if doctype else "")) 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 = '' n = ''
l = key.split('.')
series_set = False series_set = False
today = now_datetime() today = now_datetime()

for e in l:
for e in parts:
part = '' part = ''
if e.startswith('#'): if e.startswith('#'):
if not series_set: if not series_set:
@@ -120,6 +123,7 @@ def make_autoname(key='', doctype='', doc=''):


if isinstance(part, basestring): if isinstance(part, basestring):
n+=part n+=part

return n return n


def getseries(key, digits, doctype=''): def getseries(key, digits, doctype=''):


+ 1
- 1
frappe/patches.txt 查看文件

@@ -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', 'docfield', force=True) #2017-03-03
execute:frappe.reload_doc('core', 'doctype', 'docperm') #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.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 frappe.patches.v8_0.update_published_in_global_search
execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'custom_docperm')
execute:frappe.reload_doc('core', 'doctype', 'deleted_document') execute:frappe.reload_doc('core', 'doctype', 'deleted_document')


+ 2
- 0
frappe/patches/v8_0/update_records_in_global_search.py 查看文件

@@ -1,5 +1,7 @@
import frappe
from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype


def execute(): def execute():
frappe.cache().delete_value('doctypes_with_global_search')
for doctype in get_doctypes_with_global_search(with_child_tables=False): for doctype in get_doctypes_with_global_search(with_child_tables=False):
rebuild_for_doctype(doctype) rebuild_for_doctype(doctype)

+ 2
- 1
frappe/permissions.py 查看文件

@@ -330,7 +330,8 @@ def get_all_perms(role):
'''Returns valid permissions for a given role''' '''Returns valid permissions for a given role'''
perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role)) perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role))
custom_perms = frappe.get_all('Custom 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: for p in perms:
if p.parent not in doctypes_with_custom_perms: if p.parent not in doctypes_with_custom_perms:


+ 2
- 0
frappe/public/build.json 查看文件

@@ -30,6 +30,7 @@
"public/js/frappe/ui/field_group.js", "public/js/frappe/ui/field_group.js",
"public/js/frappe/form/control.js", "public/js/frappe/form/control.js",
"public/js/frappe/form/link_selector.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/dialog.js"
], ],
"css/desk.min.css": [ "css/desk.min.css": [
@@ -104,6 +105,7 @@
"public/js/frappe/ui/field_group.js", "public/js/frappe/ui/field_group.js",
"public/js/frappe/form/control.js", "public/js/frappe/form/control.js",
"public/js/frappe/form/link_selector.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/dialog.js",
"public/js/frappe/ui/app_icon.js", "public/js/frappe/ui/app_icon.js",




+ 7
- 0
frappe/public/css/desk.css 查看文件

@@ -978,6 +978,13 @@ input[type="checkbox"]:checked:before {
font-size: 13px; font-size: 13px;
color: #3b99fc; color: #3b99fc;
} }
.multiselect-empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
@-moz-document url-prefix() { @-moz-document url-prefix() {
input[type="checkbox"] { input[type="checkbox"] {
visibility: visible; visibility: visible;


+ 3
- 2
frappe/public/js/frappe/form/footer/timeline.js 查看文件

@@ -447,8 +447,9 @@ frappe.ui.form.Timeline = Class.extend({
var parts = [], count = 0; var parts = [], count = 0;
data.row_changed.every(function(row) { data.row_changed.every(function(row) {
row[3].every(function(p) { 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) { if(df && !df.hidden) {
field_display_status = frappe.perm.get_field_display_status(df, field_display_status = frappe.perm.get_field_display_status(df,


+ 212
- 0
frappe/public/js/frappe/form/multi_select_dialog.js 查看文件

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

});

+ 1
- 1
frappe/public/js/frappe/form/quick_entry.js 查看文件

@@ -31,7 +31,7 @@ frappe.ui.form.quick_entry = function(doctype, success) {
} }


var dialog = new frappe.ui.Dialog({ var dialog = new frappe.ui.Dialog({
title: __("New {0}", [doctype]),
title: __("New {0}", [__(doctype)]),
fields: mandatory, fields: mandatory,
}); });




+ 1
- 1
frappe/public/js/frappe/list/list_renderer.js 查看文件

@@ -144,7 +144,7 @@ frappe.views.ListRenderer = Class.extend({
} }
// kanban column fields // kanban column fields
if (me.meta.__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 () { set_columns: function () {


+ 5
- 0
frappe/public/js/frappe/model/model.js 查看文件

@@ -187,6 +187,11 @@ $.extend(frappe.model, {
return txt.replace(/ /g, "_").toLowerCase(); 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) { can_create: function(doctype) {
return frappe.boot.user.can_create.indexOf(doctype)!==-1; return frappe.boot.user.can_create.indexOf(doctype)!==-1;
}, },


+ 0
- 1
frappe/public/js/frappe/ui/field_group.js 查看文件

@@ -67,7 +67,6 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({
var f = this.fields_dict[key]; var f = this.fields_dict[key];
if(f.get_parsed_value) { if(f.get_parsed_value) {
var v = f.get_parsed_value(); var v = f.get_parsed_value();

if(f.df.reqd && is_null(v)) if(f.df.reqd && is_null(v))
errors.push(__(f.df.label)); errors.push(__(f.df.label));




+ 23
- 18
frappe/public/js/frappe/ui/keyboard.js 查看文件

@@ -8,7 +8,6 @@ frappe.ui.keys.setup = function() {
for(var i=0, l = frappe.ui.keys.handlers[key].length; i<l; i++) { for(var i=0, l = frappe.ui.keys.handlers[key].length; i<l; i++) {
var handler = frappe.ui.keys.handlers[key][i]; var handler = frappe.ui.keys.handlers[key][i];
var _out = handler.apply(this, [e]); var _out = handler.apply(this, [e]);

if(_out===false) { if(_out===false) {
out = _out; out = _out;
} }
@@ -23,10 +22,9 @@ frappe.ui.keys.get_key = function(e) {
//safari doesn't have key property //safari doesn't have key property
if(!key) { if(!key) {
var keycode = e.keyCode || e.which; 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 // ArrowDown -> down
key = key.substr(5).toLowerCase(); 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) { frappe.ui.keys.on('Enter', function(e) {
if(cur_dialog && cur_dialog.confirm_dialog) { if(cur_dialog && cur_dialog.confirm_dialog) {
@@ -127,4 +116,20 @@ frappe.ui.keyCode = {
ENTER: 13, ENTER: 13,
TAB: 9, TAB: 9,
SPACE: 32 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;
}
} }

+ 30
- 13
frappe/public/js/frappe/ui/sort_selector.js 查看文件

@@ -91,16 +91,12 @@ frappe.ui.SortSelector = Class.extend({
var me = this; var me = this;
var meta = frappe.get_meta(this.doctype); 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(!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 { } else {
// default // default
this.args.sort_by = 'modified'; this.args.sort_by = 'modified';
@@ -115,7 +111,7 @@ frappe.ui.SortSelector = Class.extend({
if(!this.args.options) { if(!this.args.options) {
// default options // default options
var _options = [ var _options = [
{'fieldname': 'modified'},
{'fieldname': 'modified'}
] ]


// title field // 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 // de-duplicate
this.args.options = _options.uniqBy(function(obj) { 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_by = this.args.sort_by;
this.sort_order = this.args.sort_order; 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) { get_label: function(fieldname) {
if(fieldname==='idx') { if(fieldname==='idx') {
return __("Most Used"); return __("Most Used");


+ 1
- 5
frappe/public/js/frappe/ui/toolbar/search_utils.js 查看文件

@@ -575,10 +575,6 @@ frappe.search.utils = {
return rendered; 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();});
},
} }

+ 9
- 0
frappe/public/js/frappe/views/kanban/kanban_board.js 查看文件

@@ -41,6 +41,12 @@ frappe.provide("frappe.views");
columns: columns, columns: columns,
cur_list: opts.cur_list 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) { update_cards: function (updater, cards) {
@@ -1038,6 +1044,9 @@ frappe.provide("frappe.views");
function is_filters_modified(board, cur_list) { function is_filters_modified(board, cur_list) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
setTimeout(function() { 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()); var list_filters = JSON.stringify(cur_list.filter_list.get_filters());
resolve(list_filters !== board.filters); resolve(list_filters !== board.filters);
}, 2000); }, 2000);


+ 7
- 4
frappe/public/js/frappe/views/reports/reportview.js 查看文件

@@ -227,7 +227,7 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({


set_route_filters: function(first_load) { set_route_filters: function(first_load) {
var me = this; var me = this;
if(frappe.route_options && !this.user_settings.filters) {
if(frappe.route_options) {
this.set_filters_from_route_options(); this.set_filters_from_route_options();
return true; return true;
} else if(this.user_settings } 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) { $.each(frappe.model.get_all_docs(doc), function(i, d) {
// find the document of the current updated record // find the document of the current updated record
// from locals (which is synced in the response) // 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 if(frappe.model.std_fields_list.indexOf(k)===-1
&& item[k]!==undefined) { && item[k]!==undefined) {
new_item[k] = v; new_item[k] = v;


+ 11
- 2
frappe/public/less/desk.less 查看文件

@@ -431,7 +431,7 @@ textarea.form-control {
flex: 0 0 36px; flex: 0 0 36px;
order: -1; order: -1;
justify-content: flex-end; justify-content: flex-end;
input[type="checkbox"] { input[type="checkbox"] {
margin-right: 0; 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() { @-moz-document url-prefix() {
input[type="checkbox"] { input[type="checkbox"] {
visibility: visible; visibility: visible;


+ 1
- 1
frappe/utils/global_search.py 查看文件

@@ -54,7 +54,7 @@ def rebuild_for_doctype(doctype):
:param doctype: Doctype ''' :param doctype: Doctype '''


def _get_filters(): def _get_filters():
filters = frappe._dict({ "docstatus": ["!=", 1] })
filters = frappe._dict({ "docstatus": ["!=", 2] })
if meta.has_field("enabled"): if meta.has_field("enabled"):
filters.enabled = 1 filters.enabled = 1
if meta.has_field("disabled"): if meta.has_field("disabled"):


+ 1
- 1
frappe/utils/install.py 查看文件

@@ -105,7 +105,7 @@ def import_country_and_currency():
country = frappe._dict(data[name]) country = frappe._dict(data[name])
add_country_and_currency(name, country) add_country_and_currency(name, country)


print
print()


# enable frequently used currencies # enable frequently used currencies
for currency in ("INR", "USD", "GBP", "EUR", "AED", "AUD", "JPY", "CNY", "CHF"): for currency in ("INR", "USD", "GBP", "EUR", "AED", "AUD", "JPY", "CNY", "CHF"):


+ 22
- 9
frappe/utils/xlsxutils.py 查看文件

@@ -9,7 +9,6 @@ import openpyxl
from cStringIO import StringIO from cStringIO import StringIO
from openpyxl.styles import Font from openpyxl.styles import Font


import html2text


# return xlsx file object # return xlsx file object
def make_xlsx(data, sheet_name): def make_xlsx(data, sheet_name):
@@ -24,19 +23,33 @@ def make_xlsx(data, sheet_name):
clean_row = [] clean_row = []
for item in row: for item in row:
if isinstance(item, basestring): 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: else:
value = item value = item

clean_row.append(value) clean_row.append(value)


ws.append(clean_row) ws.append(clean_row)


xlsx_file = StringIO() xlsx_file = StringIO()
wb.save(xlsx_file) 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]

Loading…
取消
儲存