Ver código fonte

Merge branch 'develop' into staging

version-14
mbauskar 7 anos atrás
pai
commit
14533ed2c5
77 arquivos alterados com 2847 adições e 3569 exclusões
  1. +6
    -31
      frappe/__init__.py
  2. +16
    -9
      frappe/commands/utils.py
  3. +2
    -0
      frappe/contacts/doctype/contact/contact.py
  4. +1
    -1
      frappe/core/doctype/doctype/doctype.py
  5. +28
    -1
      frappe/core/doctype/domain_settings/domain_settings.py
  6. +1
    -0
      frappe/core/doctype/module_def/module_def.py
  7. +23
    -0
      frappe/core/doctype/user/test_user.js
  8. +4
    -4
      frappe/core/doctype/user/user.js
  9. +31
    -1
      frappe/core/doctype/user/user.json
  10. +2
    -2
      frappe/core/doctype/user/user.py
  11. +2
    -2
      frappe/core/page/permission_manager/permission_manager.js
  12. +5
    -2
      frappe/core/page/permission_manager/permission_manager.py
  13. +1
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  14. +4
    -3
      frappe/desk/reportview.py
  15. +1
    -0
      frappe/hooks.py
  16. +61
    -3
      frappe/public/build.json
  17. +6
    -0
      frappe/public/css/email.css
  18. +1
    -1
      frappe/public/js/frappe/dom.js
  19. +0
    -2270
      frappe/public/js/frappe/form/control.js
  20. +187
    -0
      frappe/public/js/frappe/form/controls/attach.js
  21. +66
    -0
      frappe/public/js/frappe/form/controls/attach_image.js
  22. +172
    -0
      frappe/public/js/frappe/form/controls/base_control.js
  23. +180
    -0
      frappe/public/js/frappe/form/controls/base_input.js
  24. +38
    -0
      frappe/public/js/frappe/form/controls/button.js
  25. +38
    -0
      frappe/public/js/frappe/form/controls/check.js
  26. +8
    -0
      frappe/public/js/frappe/form/controls/code.js
  27. +83
    -0
      frappe/public/js/frappe/form/controls/color.js
  28. +20
    -0
      frappe/public/js/frappe/form/controls/currency.js
  29. +138
    -0
      frappe/public/js/frappe/form/controls/data.js
  30. +106
    -0
      frappe/public/js/frappe/form/controls/date.js
  31. +62
    -0
      frappe/public/js/frappe/form/controls/date_range.js
  32. +23
    -0
      frappe/public/js/frappe/form/controls/datetime.js
  33. +21
    -0
      frappe/public/js/frappe/form/controls/dynamic_link.js
  34. +27
    -0
      frappe/public/js/frappe/form/controls/float.js
  35. +5
    -0
      frappe/public/js/frappe/form/controls/heading.js
  36. +26
    -0
      frappe/public/js/frappe/form/controls/html.js
  37. +22
    -0
      frappe/public/js/frappe/form/controls/image.js
  38. +25
    -0
      frappe/public/js/frappe/form/controls/int.js
  39. +382
    -0
      frappe/public/js/frappe/form/controls/link.js
  40. +52
    -0
      frappe/public/js/frappe/form/controls/password.js
  41. +8
    -0
      frappe/public/js/frappe/form/controls/read_only.js
  42. +64
    -0
      frappe/public/js/frappe/form/controls/select.js
  43. +120
    -0
      frappe/public/js/frappe/form/controls/signature.js
  44. +30
    -0
      frappe/public/js/frappe/form/controls/table.js
  45. +20
    -0
      frappe/public/js/frappe/form/controls/text.js
  46. +297
    -0
      frappe/public/js/frappe/form/controls/text_editor.js
  47. +44
    -0
      frappe/public/js/frappe/form/controls/time.js
  48. +3
    -6
      frappe/public/js/frappe/form/footer/attachments.js
  49. +1
    -1
      frappe/public/js/frappe/form/footer/timeline.js
  50. +1
    -1
      frappe/public/js/frappe/form/footer/timeline_item.html
  51. +4
    -0
      frappe/public/js/frappe/form/save.js
  52. +7
    -0
      frappe/public/js/frappe/form/toolbar.js
  53. +27
    -3
      frappe/public/js/frappe/list/list_renderer.js
  54. +1
    -1
      frappe/public/js/frappe/misc/tools.js
  55. +1
    -1
      frappe/public/js/frappe/ui/page.js
  56. +7
    -1
      frappe/public/js/frappe/ui/toolbar/search_utils.js
  57. +9
    -2
      frappe/public/js/frappe/views/communication.js
  58. +91
    -0
      frappe/public/js/frappe/views/reports/print_tree.html
  59. +20
    -1
      frappe/public/js/frappe/views/treeview.js
  60. +14
    -6
      frappe/public/js/legacy/form.js
  61. +4
    -4
      frappe/public/js/lib/fullcalendar/fullcalendar.min.css
  62. +8
    -7
      frappe/public/js/lib/fullcalendar/fullcalendar.min.js
  63. +3
    -3
      frappe/public/js/lib/fullcalendar/fullcalendar.print.css
  64. +7
    -6
      frappe/public/js/lib/fullcalendar/gcal.js
  65. +5
    -5
      frappe/public/js/lib/fullcalendar/locale-all.js
  66. +14
    -0
      frappe/public/js/lib/microtemplate.js
  67. +3
    -7
      frappe/public/js/lib/moment/moment-timezone-with-data.min.js
  68. +7
    -673
      frappe/public/js/lib/moment/moment-with-locales.min.js
  69. +2
    -492
      frappe/public/js/lib/moment/moment.min.js
  70. +7
    -0
      frappe/public/less/email.less
  71. +1
    -1
      frappe/sessions.py
  72. +17
    -11
      frappe/tests/test_domainification.py
  73. +2
    -0
      frappe/tests/ui/tests.txt
  74. +9
    -5
      frappe/utils/user.py
  75. +1
    -1
      frappe/website/js/website.js
  76. +59
    -0
      frappe/workflow/doctype/workflow/tests/test_workflow_create.js
  77. +53
    -0
      frappe/workflow/doctype/workflow/tests/test_workflow_test.js

+ 6
- 31
frappe/__init__.py Ver arquivo

@@ -481,6 +481,7 @@ def clear_cache(user=None, doctype=None):
:param user: If user is given, only user cache is cleared.
:param doctype: If doctype is given, only DocType cache is cleared."""
import frappe.sessions
from frappe.core.doctype.domain_settings.domain_settings import clear_domain_cache
if doctype:
import frappe.model.meta
frappe.model.meta.clear_cache(doctype)
@@ -492,7 +493,7 @@ def clear_cache(user=None, doctype=None):
frappe.sessions.clear_cache()
translate.clear_cache()
reset_metadata_version()
clear_domainification_cache()
clear_domain_cache()
local.cache = {}
local.new_doc_templates = {}

@@ -1358,37 +1359,11 @@ def safe_eval(code, eval_globals=None, eval_locals=None):

return eval(code, eval_globals, eval_locals)

def get_active_domains():
""" get the domains set in the Domain Settings as active domain """

active_domains = cache().hget("domains", "active_domains") or None
if active_domains is None:
domains = get_all("Has Domain", filters={ "parent": "Domain Settings" },
fields=["domain"], distinct=True)

active_domains = [row.get("domain") for row in domains]
active_domains.append("")
cache().hset("domains", "active_domains", active_domains)

return active_domains

def get_active_modules():
""" get the active modules from Module Def"""
active_modules = cache().hget("modules", "active_modules") or None
if active_modules is None:
domains = get_active_domains()
modules = get_all("Module Def", filters={"restrict_to_domain": ("in", domains)})
active_modules = [module.name for module in modules]
cache().hset("modules", "active_modules", active_modules)

return active_modules

def clear_domainification_cache():
_cache = cache()
_cache.delete_key("domains", "active_domains")
_cache.delete_key("modules", "active_modules")

def get_system_settings(key):
if not local.system_settings.has_key(key):
local.system_settings.update({key: db.get_single_value('System Settings', key)})
return local.system_settings.get(key)

def get_active_domains():
from frappe.core.doctype.domain_settings.domain_settings import get_active_domains
return get_active_domains()

+ 16
- 9
frappe/commands/utils.py Ver arquivo

@@ -360,9 +360,10 @@ def serve(context, port=None, profile=False, sites_path='.', site=None):
frappe.app.serve(port=port, profile=profile, site=site, sites_path='.')

@click.command('request')
@click.argument('args')
@click.option('--args', help='arguments like `?cmd=test&key=value` or `/api/request/method?..`')
@click.option('--path', help='path to request JSON')
@pass_context
def request(context, args):
def request(context, args=None, path=None):
"Run a request as an admin"
import frappe.handler
import frappe.api
@@ -370,13 +371,19 @@ def request(context, args):
try:
frappe.init(site=site)
frappe.connect()
if "?" in args:
frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")])
else:
frappe.local.form_dict = frappe._dict()

if args.startswith("/api/method"):
frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1]
if args:
if "?" in args:
frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")])
else:
frappe.local.form_dict = frappe._dict()

if args.startswith("/api/method"):
frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1]
elif path:
with open(os.path.join('..', path), 'r') as f:
args = json.loads(f.read())

frappe.local.form_dict = frappe._dict(args)

frappe.handler.execute_cmd(frappe.form_dict.cmd)



+ 2
- 0
frappe/contacts/doctype/contact/contact.py Ver arquivo

@@ -22,6 +22,8 @@ class Contact(Document):
break

def validate(self):
if self.email_id:
self.email_id = self.email_id.strip()
self.set_user()
if self.email_id and not self.image:
self.image = has_gravatar(self.email_id)


+ 1
- 1
frappe/core/doctype/doctype/doctype.py Ver arquivo

@@ -407,7 +407,7 @@ def validate_fields(meta):
validate_column_name(fieldname)

def check_unique_fieldname(fieldname):
duplicates = filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields))
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
if len(duplicates) > 1:
frappe.throw(_("Fieldname {0} appears multiple times in rows {1}").format(fieldname, ", ".join(duplicates)))



+ 28
- 1
frappe/core/doctype/domain_settings/domain_settings.py Ver arquivo

@@ -8,4 +8,31 @@ from frappe.model.document import Document

class DomainSettings(Document):
def on_update(self):
frappe.clear_domainification_cache()
clear_domain_cache()

def get_active_domains():
""" get the domains set in the Domain Settings as active domain """
def _get_active_domains():
domains = frappe.get_all("Has Domain", filters={ "parent": "Domain Settings" },
fields=["domain"], distinct=True)

active_domains = [row.get("domain") for row in domains]
active_domains.append("")
return active_domains

return frappe.cache().get_value("active_domains", _get_active_domains)

def get_active_modules():
""" get the active modules from Module Def"""
def _get_active_modules():
active_modules = []
active_domains = get_active_domains()
for m in frappe.get_all("Module Def", fields=['name', 'restrict_to_domain']):
if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains):
active_modules.append(m.name)
return active_modules

return frappe.cache().get_value('active_modules', _get_active_modules)

def clear_domain_cache():
frappe.cache().delete_key(['active_domains', 'active_modules'])

+ 1
- 0
frappe/core/doctype/module_def/module_def.py Ver arquivo

@@ -10,6 +10,7 @@ class ModuleDef(Document):
def on_update(self):
"""If in `developer_mode`, create folder for module and
add in `modules.txt` of app if missing."""
frappe.clear_cache()
if frappe.conf.get("developer_mode"):
self.create_modules_folder()
self.add_to_modules_txt()


+ 23
- 0
frappe/core/doctype/user/test_user.js Ver arquivo

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line

QUnit.test("test: User", function (assert) {
let done = assert.async();

// number of asserts
assert.expect(1);

frappe.run_serially([
// insert a new User
() => frappe.tests.make('User', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);

});

+ 4
- 4
frappe/core/doctype/user/user.js Ver arquivo

@@ -60,11 +60,11 @@ frappe.ui.form.on('User', {
"user": doc.name
};
frappe.set_route('List', 'User Permission');
}, null, "btn-default")
}, __("Permissions"))

frm.add_custom_button(__('View Permitted Documents'),
() => frappe.set_route('query-report', 'Permitted Documents For User',
{user: frm.doc.name}));
{user: frm.doc.name}), __("Permissions"));

frm.toggle_display(['sb1', 'sb3', 'modules_access'], true);
}
@@ -76,7 +76,7 @@ frappe.ui.form.on('User', {
"user": frm.doc.name
}
})
})
}, __("Password"));

frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({
@@ -85,7 +85,7 @@ frappe.ui.form.on('User', {
"user": frm.doc.name
}
})
})
}, __("Password"));

frm.trigger('enabled');



+ 31
- 1
frappe/core/doctype/user/user.json Ver arquivo

@@ -1043,6 +1043,36 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "send_me_a_copy",
"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": "Send Me A Copy of Outgoing Emails",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -1971,7 +2001,7 @@
"istable": 0,
"max_attachments": 5,
"menu_index": 0,
"modified": "2017-07-07 17:18:14.047969",
"modified": "2017-08-23 10:34:11.944298",
"modified_by": "Administrator",
"module": "Core",
"name": "User",


+ 2
- 2
frappe/core/doctype/user/user.py Ver arquivo

@@ -952,7 +952,7 @@ def send_token_via_email(tmp_id,token=None):
delayed=False, retry=3)

return True
@frappe.whitelist(allow_guest=True)
def reset_otp_secret(user):
otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
@@ -964,7 +964,7 @@ def reset_otp_secret(user):
'recipients':user_email, 'sender':None, 'subject':'OTP Secret Reset - {}'.format(otp_issuer or "Frappe Framework"),
'message':'<p>Your OTP secret on {} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.</p>'.format(otp_issuer or "Frappe Framework"),
'delayed':False,
'retry':3
'retry':3
}
enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args)
return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login."))


+ 2
- 2
frappe/core/page/permission_manager/permission_manager.js Ver arquivo

@@ -46,13 +46,13 @@ frappe.PermissionEngine = Class.extend({
var me = this;
this.doctype_select
= this.wrapper.page.add_select(__("Document Types"),
[{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes.sort()))
[{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes))
.change(function() {
frappe.set_route("permission-manager", $(this).val());
});
this.role_select
= this.wrapper.page.add_select(__("Roles"),
[__("Select Role")+"..."].concat(this.options.roles.sort()))
[__("Select Role")+"..."].concat(this.options.roles))
.change(function() {
me.refresh();
});


+ 5
- 2
frappe/core/page/permission_manager/permission_manager.py Ver arquivo

@@ -35,9 +35,12 @@ def get_roles_and_doctypes():
"restrict_to_domain": ("in", active_domains)
}, fields=["name"])

doctypes_list = [ {"label":_(d.get("name")), "value":d.get("name")} for d in doctypes]
roles_list = [ {"label":_(d.get("name")), "value":d.get("name")} for d in roles]

return {
"doctypes": [d.get("name") for d in doctypes],
"roles": [d.get("name") for d in roles]
"doctypes": sorted(doctypes_list, key=lambda d: d['label']),
"roles": sorted(roles_list, key=lambda d: d['label'])
}

@frappe.whitelist()


+ 1
- 1
frappe/desk/page/setup_wizard/setup_wizard.py Ver arquivo

@@ -10,7 +10,7 @@ from frappe.geo.country_info import get_country_info
from frappe.utils.file_manager import save_file
from frappe.utils.password import update_password
from werkzeug.useragents import UserAgent
import install_fixtures
from . import install_fixtures
from six import string_types

@frappe.whitelist()


+ 4
- 3
frappe/desk/reportview.py Ver arquivo

@@ -10,7 +10,7 @@ import frappe.permissions
import MySQLdb
from frappe.model.db_query import DatabaseQuery
from frappe import _
from six import text_type, string_types
from six import text_type, string_types, StringIO

@frappe.whitelist()
def get():
@@ -146,13 +146,14 @@ def export_query():

# convert to csv
import csv
from six import StringIO
from frappe.utils.xlsxutils import handle_html

f = StringIO()
writer = csv.writer(f)
for r in data:
# encode only unicode type strings and not int, floats etc.
writer.writerow(map(lambda v: isinstance(v, text_type) and v.encode('utf-8') or v, r))
writer.writerow(map(lambda v: isinstance(v, string_types) and
handle_html(v.encode('utf-8')) or v, r))

f.seek(0)
frappe.response['result'] = text_type(f.read(), 'utf-8')


+ 1
- 0
frappe/hooks.py Ver arquivo

@@ -27,6 +27,7 @@ app_include_js = [
"assets/js/desk.min.js",
"assets/js/list.min.js",
"assets/js/form.min.js",
"assets/js/control.min.js",
"assets/js/report.min.js",
"assets/js/d3.min.js",
"assets/frappe/js/frappe/toolbar.js"


+ 61
- 3
frappe/public/build.json Ver arquivo

@@ -22,16 +22,74 @@
"website/js/website.js",
"public/js/frappe/misc/rating_icons.html"
],
"js/control.min.js": [
"public/js/frappe/form/controls/base_control.js",
"public/js/frappe/form/controls/base_input.js",
"public/js/frappe/form/controls/data.js",
"public/js/frappe/form/controls/int.js",
"public/js/frappe/form/controls/float.js",
"public/js/frappe/form/controls/currency.js",
"public/js/frappe/form/controls/date.js",
"public/js/frappe/form/controls/time.js",
"public/js/frappe/form/controls/datetime.js",
"public/js/frappe/form/controls/date_range.js",
"public/js/frappe/form/controls/select.js",
"public/js/frappe/form/controls/link.js",
"public/js/frappe/form/controls/dynamic_link.js",
"public/js/frappe/form/controls/text.js",
"public/js/frappe/form/controls/code.js",
"public/js/frappe/form/controls/text_editor.js",
"public/js/frappe/form/controls/check.js",
"public/js/frappe/form/controls/image.js",
"public/js/frappe/form/controls/attach.js",
"public/js/frappe/form/controls/attach_image.js",
"public/js/frappe/form/controls/table.js",
"public/js/frappe/form/controls/color.js",
"public/js/frappe/form/controls/signature.js",
"public/js/frappe/form/controls/password.js",
"public/js/frappe/form/controls/read_only.js",
"public/js/frappe/form/controls/button.js",
"public/js/frappe/form/controls/html.js",
"public/js/frappe/form/controls/heading.js"
],
"js/dialog.min.js": [
"public/js/frappe/dom.js",
"public/js/frappe/ui/modal.html",
"public/js/frappe/form/formatters.js",
"public/js/frappe/form/layout.js",
"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/dialog.js",

"public/js/frappe/form/controls/base_control.js",
"public/js/frappe/form/controls/base_input.js",
"public/js/frappe/form/controls/data.js",
"public/js/frappe/form/controls/int.js",
"public/js/frappe/form/controls/float.js",
"public/js/frappe/form/controls/currency.js",
"public/js/frappe/form/controls/date.js",
"public/js/frappe/form/controls/time.js",
"public/js/frappe/form/controls/datetime.js",
"public/js/frappe/form/controls/date_range.js",
"public/js/frappe/form/controls/select.js",
"public/js/frappe/form/controls/link.js",
"public/js/frappe/form/controls/dynamic_link.js",
"public/js/frappe/form/controls/text.js",
"public/js/frappe/form/controls/code.js",
"public/js/frappe/form/controls/text_editor.js",
"public/js/frappe/form/controls/check.js",
"public/js/frappe/form/controls/image.js",
"public/js/frappe/form/controls/attach.js",
"public/js/frappe/form/controls/attach_image.js",
"public/js/frappe/form/controls/table.js",
"public/js/frappe/form/controls/color.js",
"public/js/frappe/form/controls/signature.js",
"public/js/frappe/form/controls/password.js",
"public/js/frappe/form/controls/read_only.js",
"public/js/frappe/form/controls/button.js",
"public/js/frappe/form/controls/html.js",
"public/js/frappe/form/controls/heading.js"
],
"css/desk.min.css": [
"public/js/lib/datepicker/datepicker.min.css",
@@ -108,7 +166,6 @@
"public/js/frappe/ui/iconbar.js",
"public/js/frappe/form/layout.js",
"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",
@@ -289,6 +346,7 @@
"public/js/frappe/views/reports/query_report.js",
"public/js/frappe/views/reports/grid_report.js",
"public/js/frappe/views/reports/print_grid.html",
"public/js/frappe/views/reports/print_tree.html",

"public/js/lib/slickgrid/jquery.event.drag.js",
"public/js/lib/slickgrid/plugins/slick.cellrangedecorator.js",


+ 6
- 0
frappe/public/css/email.css Ver arquivo

@@ -157,6 +157,12 @@ hr {
.indicator.indicator-yellow {
background-color: #FEEF72;
}
.screenshot {
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #d1d8dd;
margin: 8px 0;
max-width: 100%;
}
/* auto email report */
.report-title {
margin-bottom: 20px;


+ 1
- 1
frappe/public/js/frappe/dom.js Ver arquivo

@@ -224,7 +224,7 @@ frappe.get_modal = function(title, content) {
(function($) {
$.fn.add_options = function(options_list) {
// create options
for(var i=0; i<options_list.length; i++) {
for(var i=0, j=options_list.length; i<j; i++) {
var v = options_list[i];
if (is_null(v)) {
var value = null;


+ 0
- 2270
frappe/public/js/frappe/form/control.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 187
- 0
frappe/public/js/frappe/form/controls/attach.js Ver arquivo

@@ -0,0 +1,187 @@
frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
make_input: function() {
var me = this;
this.$input = $('<button class="btn btn-default btn-sm btn-attach">')
.html(__("Attach"))
.prependTo(me.input_area)
.on("click", function() {
me.onclick();
});
this.$value = $('<div style="margin-top: 5px;">\
<div class="ellipsis" style="display: inline-block; width: 90%;">\
<i class="fa fa-paper-clip"></i> \
<a class="attached-file" target="_blank"></a>\
</div>\
<a class="close">&times;</a></div>')
.prependTo(me.input_area)
.toggle(false);
this.input = this.$input.get(0);
this.set_input_attributes();
this.has_input = true;

this.$value.find(".close").on("click", function() {
me.clear_attachment();
});
},
clear_attachment: function() {
var me = this;
if(this.frm) {
me.frm.attachments.remove_attachment_by_filename(me.value, function() {
me.parse_validate_and_set_in_model(null);
me.refresh();
me.frm.save();
});
} else {
this.dataurl = null;
this.fileobj = null;
this.set_input(null);
this.refresh();
}
},
onclick: function() {
var me = this;
if(this.doc) {
var doc = this.doc.parent && frappe.model.get_doc(this.doc.parenttype, this.doc.parent) || this.doc;
if (doc.__islocal) {
frappe.msgprint(__("Please save the document before uploading."));
return;
}
}
if(!this.dialog) {
this.dialog = new frappe.ui.Dialog({
title: __(this.df.label || __("Upload")),
fields: [
{fieldtype:"HTML", fieldname:"upload_area"},
{fieldtype:"HTML", fieldname:"or_attach", options: __("Or")},
{fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") },
{fieldtype:"Button", fieldname:"clear",
label:__("Clear Attachment"), click: function() {
me.clear_attachment();
me.dialog.hide();
}
},
]
});
}

this.dialog.show();

this.dialog.get_field("upload_area").$wrapper.empty();

// select from existing attachments
var attachments = this.frm && this.frm.attachments.get_attachments() || [];
var select = this.dialog.get_field("select");
if(attachments.length) {
attachments = $.map(attachments, function(o) { return o.file_url; });
select.df.options = [""].concat(attachments);
select.toggle(true);
this.dialog.get_field("or_attach").toggle(true);
select.refresh();
} else {
this.dialog.get_field("or_attach").toggle(false);
select.toggle(false);
}
select.$input.val("");

// show button if attachment exists
this.dialog.get_field('clear').$wrapper.toggle(this.get_model_value() ? true : false);

this.set_upload_options();
frappe.upload.make(this.upload_options);
},

set_upload_options: function() {
var me = this;
this.upload_options = {
parent: this.dialog.get_field("upload_area").$wrapper,
args: {},
allow_multiple: 0,
max_width: this.df.max_width,
max_height: this.df.max_height,
options: this.df.options,
btn: this.dialog.set_primary_action(__("Upload")),
on_no_attach: function() {
// if no attachmemts,
// check if something is selected
var selected = me.dialog.get_field("select").get_value();
if(selected) {
me.parse_validate_and_set_in_model(selected);
me.dialog.hide();
me.frm.save();
} else {
frappe.msgprint(__("Please attach a file or set a URL"));
}
},
callback: function(attachment) {
me.on_upload_complete(attachment);
me.dialog.hide();
},
onerror: function() {
me.dialog.hide();
}
};

if ("is_private" in this.df) {
this.upload_options.is_private = this.df.is_private;
}

if(this.frm) {
this.upload_options.args = {
from_form: 1,
doctype: this.frm.doctype,
docname: this.frm.docname
};
} else {
this.upload_options.on_attach = function(fileobj, dataurl) {
me.dialog.hide();
me.fileobj = fileobj;
me.dataurl = dataurl;
if(me.on_attach) {
me.on_attach();
}
if(me.df.on_attach) {
me.df.on_attach(fileobj, dataurl);
}
me.on_upload_complete();
};
}
},

set_input: function(value, dataurl) {
this.value = value;
if(this.value) {
this.$input.toggle(false);
if(this.value.indexOf(",")!==-1) {
var parts = this.value.split(",");
var filename = parts[0];
dataurl = parts[1];
}
this.$value.toggle(true).find(".attached-file")
.html(filename || this.value)
.attr("href", dataurl || this.value);
} else {
this.$input.toggle(true);
this.$value.toggle(false);
}
},

get_value: function() {
if(this.frm) {
return this.value;
} else {
return this.fileobj ? (this.fileobj.filename + "," + this.dataurl) : null;
}
},

on_upload_complete: function(attachment) {
if(this.frm) {
this.parse_validate_and_set_in_model(attachment.file_url);
this.refresh();
this.frm.attachments.update_attachment(attachment);
this.frm.save();
} else {
this.value = this.get_value();
this.refresh();
}
},
});

+ 66
- 0
frappe/public/js/frappe/form/controls/attach_image.js Ver arquivo

@@ -0,0 +1,66 @@
frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({
make: function() {
var me = this;
this._super();

this.container = $('<div class="control-container">').insertAfter($(this.wrapper));
$(this.wrapper).detach();
this.container.attr('data-fieldtype', this.df.fieldtype).append(this.wrapper);
if(this.df.align === 'center') {
this.container.addClass("flex-justify-center");
} else if (this.df.align === 'right') {
this.container.addClass("flex-justify-end");
}

this.img_wrapper = $('<div style="width: 100%; height: calc(100% - 40px); position: relative;">\
<div class="missing-image attach-missing-image"><i class="octicon octicon-device-camera"></i></div></div>')
.appendTo(this.wrapper);

this.img_container = $(`<div class='img-container'></div>`);
this.img = $(`<img class='img-responsive attach-image-display'>`)
.appendTo(this.img_container);

this.img_overlay = $(`<div class='img-overlay'>
<span class="overlay-text">Change</span>
</div>`).appendTo(this.img_container);

this.remove_image_link = $('<a style="font-size: 12px;color: #8D99A6;">Remove</a>');

this.img_wrapper.append(this.img_container).append(this.remove_image_link);
// this.img.toggle(false);
// this.img_overlay.toggle(false);
this.img_container.toggle(false);
this.remove_image_link.toggle(false);

// propagate click to Attach button
this.img_wrapper.find(".missing-image").on("click", function() { me.$input.click(); });
this.img_container.on("click", function() { me.$input.click(); });
this.remove_image_link.on("click", function() { me.$value.find(".close").click(); });

this.set_image();
},
refresh_input: function() {
this._super();
$(this.wrapper).find('.btn-attach').addClass('hidden');
this.set_image();
if(this.get_status()=="Read") {
$(this.disp_area).toggle(false);
}
},
set_image: function() {
if(this.get_value()) {
$(this.img_wrapper).find(".missing-image").toggle(false);
// this.img.attr("src", this.dataurl ? this.dataurl : this.value).toggle(true);
// this.img_overlay.toggle(true);
this.img.attr("src", this.dataurl ? this.dataurl : this.value);
this.img_container.toggle(true);
this.remove_image_link.toggle(true);
} else {
$(this.img_wrapper).find(".missing-image").toggle(true);
// this.img.toggle(false);
// this.img_overlay.toggle(false);
this.img_container.toggle(false);
this.remove_image_link.toggle(false);
}
}
});

+ 172
- 0
frappe/public/js/frappe/form/controls/base_control.js Ver arquivo

@@ -0,0 +1,172 @@
frappe.ui.form.make_control = function (opts) {
var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, "");
if(frappe.ui.form[control_class_name]) {
return new frappe.ui.form[control_class_name](opts);
} else {
// eslint-disable-next-line
console.log("Invalid Control Name: " + opts.df.fieldtype);
}
};

frappe.ui.form.Control = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.make();

// if developer_mode=1, show fieldname as tooltip
if(frappe.boot.user && frappe.boot.user.name==="Administrator" &&
frappe.boot.developer_mode===1 && this.$wrapper) {
this.$wrapper.attr("title", __(this.df.fieldname));
}

if(this.render_input) {
this.refresh();
}
},
make: function() {
this.make_wrapper();
this.$wrapper
.attr("data-fieldtype", this.df.fieldtype)
.attr("data-fieldname", this.df.fieldname);
this.wrapper = this.$wrapper.get(0);
this.wrapper.fieldobj = this; // reference for event handlers
},

make_wrapper: function() {
this.$wrapper = $("<div class='frappe-control'></div>").appendTo(this.parent);

// alias
this.wrapper = this.$wrapper;
},

toggle: function(show) {
this.df.hidden = show ? 0 : 1;
this.refresh();
},

// returns "Read", "Write" or "None"
// as strings based on permissions
get_status: function(explain) {
if(!this.doctype && !this.docname) {
// like in case of a dialog box
if (cint(this.df.hidden)) {
// eslint-disable-next-line
if(explain) console.log("By Hidden: None");
return "None";

} else if (cint(this.df.hidden_due_to_dependency)) {
// eslint-disable-next-line
if(explain) console.log("By Hidden Dependency: None");
return "None";

} else if (cint(this.df.read_only)) {
// eslint-disable-next-line
if(explain) console.log("By Read Only: Read");
return "Read";

}

return "Write";
}

var status = frappe.perm.get_field_display_status(this.df,
frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain);

// hide if no value
if (this.doctype && status==="Read" && !this.only_input
&& is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname))
&& !in_list(["HTML", "Image"], this.df.fieldtype)) {

// eslint-disable-next-line
if(explain) console.log("By Hide Read-only, null fields: None");
status = "None";
}

return status;
},
refresh: function() {
this.disp_status = this.get_status();
this.$wrapper
&& this.$wrapper.toggleClass("hide-control", this.disp_status=="None")
&& this.refresh_input
&& this.refresh_input();
},
get_doc: function() {
return this.doctype && this.docname
&& locals[this.doctype] && locals[this.doctype][this.docname] || {};
},
get_model_value: function() {
if(this.doc) {
return this.doc[this.df.fieldname];
}
},
set_value: function(value) {
return this.validate_and_set_in_model(value);
},
parse_validate_and_set_in_model: function(value, e) {
if(this.parse) {
value = this.parse(value);
}
return this.validate_and_set_in_model(value, e);
},
validate_and_set_in_model: function(value, e) {
var me = this;
if(this.inside_change_event) {
return Promise.resolve();
}
this.inside_change_event = true;
var set = function(value) {
me.inside_change_event = false;
return frappe.run_serially([
() => me.set_model_value(value),
() => {
me.set_mandatory && me.set_mandatory(value);

if(me.df.change || me.df.onchange) {
// onchange event specified in df
return (me.df.change || me.df.onchange).apply(me, [e]);
}
}
]);
};

value = this.validate(value);
if (value && value.then) {
// got a promise
return value.then((value) => set(value));
} else {
// all clear
return set(value);
}
},
get_value: function() {
if(this.get_status()==='Write') {
return this.get_input_value ?
(this.parse ? this.parse(this.get_input_value()) : this.get_input_value()) :
undefined;
} else if(this.get_status()==='Read') {
return this.value || undefined;
} else {
return undefined;
}
},
set_model_value: function(value) {
if(this.doctype && this.docname) {
this.last_value = value;
return frappe.model.set_value(this.doctype, this.docname, this.df.fieldname,
value, this.df.fieldtype);
} else {
if(this.doc) {
this.doc[this.df.fieldname] = value;
}
this.set_input(value);
return Promise.resolve();
}
},
set_focus: function() {
if(this.$input) {
this.$input.get(0).focus();
return true;
}
}
});

+ 180
- 0
frappe/public/js/frappe/form/controls/base_input.js Ver arquivo

@@ -0,0 +1,180 @@
frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
horizontal: true,
make: function() {
// parent element
this._super();
this.set_input_areas();

// set description
this.set_max_width();
},
make_wrapper: function() {
if(this.only_input) {
this.$wrapper = $('<div class="form-group frappe-control">').appendTo(this.parent);
} else {
this.$wrapper = $('<div class="frappe-control">\
<div class="form-group">\
<div class="clearfix">\
<label class="control-label" style="padding-right: 0px;"></label>\
</div>\
<div class="control-input-wrapper">\
<div class="control-input"></div>\
<div class="control-value like-disabled-input" style="display: none;"></div>\
<p class="help-box small text-muted hidden-xs"></p>\
</div>\
</div>\
</div>').appendTo(this.parent);
}
},
toggle_label: function(show) {
this.$wrapper.find(".control-label").toggleClass("hide", !show);
},
toggle_description: function(show) {
this.$wrapper.find(".help-box").toggleClass("hide", !show);
},
set_input_areas: function() {
if(this.only_input) {
this.input_area = this.wrapper;
} else {
this.label_area = this.label_span = this.$wrapper.find("label").get(0);
this.input_area = this.$wrapper.find(".control-input").get(0);
// keep a separate display area to rendered formatted values
// like links, currencies, HTMLs etc.
this.disp_area = this.$wrapper.find(".control-value").get(0);
}
},
set_max_width: function() {
if(this.horizontal) {
this.$wrapper.addClass("input-max-width");
}
},

// update input value, label, description
// display (show/hide/read-only),
// mandatory style on refresh
refresh_input: function() {
var me = this;
var make_input = function() {
if(!me.has_input) {
me.make_input();
if(me.df.on_make) {
me.df.on_make(me);
}
}
};

var update_input = function() {
if(me.doctype && me.docname) {
me.set_input(me.value);
} else {
me.set_input(me.value || null);
}
};

if(me.disp_status != "None") {
// refresh value
if(me.doctype && me.docname) {
me.value = frappe.model.get_value(me.doctype, me.docname, me.df.fieldname);
}

if(me.disp_status=="Write") {
me.disp_area && $(me.disp_area).toggle(false);
$(me.input_area).toggle(true);
me.$input && me.$input.prop("disabled", false);
make_input();
update_input();
} else {
if(me.only_input) {
make_input();
update_input();
} else {
$(me.input_area).toggle(false);
if (me.disp_area) {
me.set_disp_area(me.value);
$(me.disp_area).toggle(true);
}
}
me.$input && me.$input.prop("disabled", true);
}

me.set_description();
me.set_label();
me.set_mandatory(me.value);
me.set_bold();
}
},

set_disp_area: function(value) {
if(in_list(["Currency", "Int", "Float"], this.df.fieldtype)
&& (this.value === 0 || value === 0)) {
// to set the 0 value in readonly for currency, int, float field
value = 0;
} else {
value = this.value || value;
}
this.disp_area && $(this.disp_area)
.html(frappe.format(value, this.df, {no_icon:true, inline:true},
this.doc || (this.frm && this.frm.doc)));
},

bind_change_event: function() {
var me = this;
this.$input && this.$input.on("change", this.change || function(e) {
me.parse_validate_and_set_in_model(me.get_input_value(), e);
});
},
bind_focusout: function() {
// on touchscreen devices, scroll to top
// so that static navbar and page head don't overlap the input
if (frappe.dom.is_touchscreen()) {
var me = this;
this.$input && this.$input.on("focusout", function() {
if (frappe.dom.is_touchscreen()) {
frappe.utils.scroll_to(me.$wrapper);
}
});
}
},
set_label: function(label) {
if(label) this.df.label = label;

if(this.only_input || this.df.label==this._label)
return;

var icon = "";
this.label_span.innerHTML = (icon ? '<i class="'+icon+'"></i> ' : "") +
__(this.df.label) || "&nbsp;";
this._label = this.df.label;
},
set_description: function(description) {
if (description !== undefined) {
this.df.description = description;
}
if (this.only_input || this.df.description===this._description) {
return;
}
if (this.df.description) {
this.$wrapper.find(".help-box").html(__(this.df.description));
} else {
this.set_empty_description();
}
this._description = this.df.description;
},
set_new_description: function(description) {
this.$wrapper.find(".help-box").html(description);
},
set_empty_description: function() {
this.$wrapper.find(".help-box").html("");
},
set_mandatory: function(value) {
this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false);
},
set_bold: function() {
if(this.$input) {
this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd));
}
if(this.disp_area) {
$(this.disp_area).toggleClass("bold", !!(this.df.bold || this.df.reqd));
}
}
});

+ 38
- 0
frappe/public/js/frappe/form/controls/button.js Ver arquivo

@@ -0,0 +1,38 @@
frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
make_input: function() {
var me = this;
this.$input = $('<button class="btn btn-default btn-xs">')
.prependTo(me.input_area)
.on("click", function() {
me.onclick();
});
this.input = this.$input.get(0);
this.set_input_attributes();
this.has_input = true;
this.toggle_label(false);
},
onclick: function() {
if(this.frm && this.frm.doc) {
if(this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) {
this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
} else {
this.frm.runscript(this.df.options, this);
}
}
else if(this.df.click) {
this.df.click();
}
},
set_input_areas: function() {
this._super();
$(this.disp_area).removeClass().addClass("hide");
},
set_empty_description: function() {
this.$wrapper.find(".help-box").empty().toggle(false);
},
set_label: function() {
$(this.label_span).html("&nbsp;");
this.$input && this.$input.html((this.df.icon ?
('<i class="'+this.df.icon+' fa-fw"></i> ') : "") + __(this.df.label));
}
});

+ 38
- 0
frappe/public/js/frappe/form/controls/check.js Ver arquivo

@@ -0,0 +1,38 @@
frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({
input_type: "checkbox",
make_wrapper: function() {
this.$wrapper = $('<div class="form-group frappe-control">\
<div class="checkbox">\
<label>\
<span class="input-area"></span>\
<span class="disp-area" style="display:none; margin-left: -20px;"></span>\
<span class="label-area small"></span>\
</label>\
<p class="help-box small text-muted"></p>\
</div>\
</div>').appendTo(this.parent);
},
set_input_areas: function() {
this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0);
this.input_area = this.$wrapper.find(".input-area").get(0);
this.disp_area = this.$wrapper.find(".disp-area").get(0);
},
make_input: function() {
this._super();
this.$input.removeClass("form-control");
},
get_input_value: function() {
return this.input && this.input.checked ? 1 : 0;
},
validate: function(value) {
return cint(value);
},
set_input: function(value) {
if(this.input) {
this.input.checked = (value ? 1 : 0);
}
this.last_value = value;
this.set_mandatory(value);
this.set_disp_area(value);
}
});

+ 8
- 0
frappe/public/js/frappe/form/controls/code.js Ver arquivo

@@ -0,0 +1,8 @@
frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
make_input: function() {
this._super();
$(this.input_area).find("textarea")
.allowTabs()
.addClass('control-code');
}
});

+ 83
- 0
frappe/public/js/frappe/form/controls/color.js Ver arquivo

@@ -0,0 +1,83 @@
frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
make_input: function () {
this._super();
this.colors = [
"#ffc4c4", "#ff8989", "#ff4d4d", "#a83333",
"#ffe8cd", "#ffd19c", "#ffb868", "#a87945",
"#ffd2c2", "#ffa685", "#ff7846", "#a85b5b",
"#ffd7d7", "#ffb1b1", "#ff8989", "#a84f2e",
"#fffacd", "#fff168", "#fff69c", "#a89f45",
"#ebf8cc", "#d9f399", "#c5ec63", "#7b933d",
"#cef6d1", "#9deca2", "#6be273", "#428b46",
"#d2f8ed", "#a4f3dd", "#77ecca", "#49937e",
"#d2f1ff", "#a6e4ff", "#78d6ff", "#4f8ea8",
"#d2d2ff", "#a3a3ff", "#7575ff", "#4d4da8",
"#dac7ff", "#b592ff", "#8e58ff", "#5e3aa8",
"#f8d4f8", "#f3aaf0", "#ec7dea", "#934f92"
];
this.make_color_input();
},
make_color_input: function () {
this.$wrapper
.find('.control-input-wrapper')
.append(`<div class="color-picker">
<div class="color-picker-pallete"></div>
</div>`);
this.$color_pallete = this.$wrapper.find('.color-picker-pallete');

var color_html = this.colors.map(this.get_color_box).join("");
this.$color_pallete.append(color_html);
this.$color_pallete.hide();
this.bind_events();
},
get_color_box: function (hex) {
return `<div class="color-box" data-color="${hex}" style="background-color: ${hex}"></div>`;
},
set_formatted_input: function(value) {
this._super(value);

if(!value) value = '#ffffff';
this.$input.css({
"background-color": value
});
},
bind_events: function () {
var mousedown_happened = false;
this.$wrapper.on("click", ".color-box", (e) => {
mousedown_happened = false;

var color_val = $(e.target).data("color");
this.set_value(color_val);
// set focus so that we can blur it later
this.set_focus();
});

this.$wrapper.find(".color-box").mousedown(() => {
mousedown_happened = true;
});

this.$input.on("focus", () => {
this.$color_pallete.show();
});
this.$input.on("blur", () => {
if (mousedown_happened) {
// cancel the blur event
mousedown_happened = false;
} else {
// blur event is okay
$(this.$color_pallete).hide();
}
});
},
validate: function (value) {
if(value === '') {
return '';
}
var is_valid = /^#[0-9A-F]{6}$/i.test(value);
if(is_valid) {
return value;
}
frappe.msgprint(__("{0} is not a valid hex color", [value]));
return null;
}
});

+ 20
- 0
frappe/public/js/frappe/form/controls/currency.js Ver arquivo

@@ -0,0 +1,20 @@
frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
format_for_input: function(value) {
var formatted_value = format_number(parseFloat(value), this.get_number_format(), this.get_precision());
return isNaN(parseFloat(value)) ? "" : formatted_value;
},

get_precision: function() {
// always round based on field precision or currency's precision
// this method is also called in this.parse()
if (!this.df.precision) {
if(frappe.boot.sysdefaults.currency_precision) {
this.df.precision = frappe.boot.sysdefaults.currency_precision;
} else {
this.df.precision = get_number_format_info(this.get_number_format()).precision;
}
}

return this.df.precision;
}
});

+ 138
- 0
frappe/public/js/frappe/form/controls/data.js Ver arquivo

@@ -0,0 +1,138 @@
frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
html_element: "input",
input_type: "text",
make_input: function() {
if(this.$input) return;

this.$input = $("<"+ this.html_element +">")
.attr("type", this.input_type)
.attr("autocomplete", "off")
.addClass("input-with-feedback form-control")
.prependTo(this.input_area);

if (in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'],
this.df.fieldtype)) {
this.$input.attr("maxlength", this.df.length || 140);
}

this.set_input_attributes();
this.input = this.$input.get(0);
this.has_input = true;
this.bind_change_event();
this.bind_focusout();
this.setup_autoname_check();

// somehow this event does not bubble up to document
// after v7, if you can debug, remove this
},
setup_autoname_check: function() {
if (!this.df.parent) return;
this.meta = frappe.get_meta(this.df.parent);
if (this.meta && this.meta.autoname
&& this.meta.autoname.substr(0, 6)==='field:') {
this.$input.on('keyup', () => {
this.set_description('');
if (this.doc && this.doc.__islocal) {
// check after 1 sec
let timeout = setTimeout(() => {
// clear any pending calls
if (this.last_check) clearTimeout(this.last_check);

// check if name exists
frappe.db.get_value(this.doctype, this.$input.val(),
'name', (val) => {
if (val) {
this.set_description(__('{0} already exists. Select another name', [val.name]));
}
});
this.last_check = null;
}, 1000);
this.last_check = timeout;
}
});
}
},
set_input_attributes: function() {
this.$input
.attr("data-fieldtype", this.df.fieldtype)
.attr("data-fieldname", this.df.fieldname)
.attr("placeholder", this.df.placeholder || "");
if(this.doctype) {
this.$input.attr("data-doctype", this.doctype);
}
if(this.df.input_css) {
this.$input.css(this.df.input_css);
}
if(this.df.input_class) {
this.$input.addClass(this.df.input_class);
}
},
set_input: function(value) {
this.last_value = this.value;
this.value = value;
this.set_formatted_input(value);
this.set_disp_area(value);
this.set_mandatory && this.set_mandatory(value);
},
set_formatted_input: function(value) {
this.$input && this.$input.val(this.format_for_input(value));
},
get_input_value: function() {
return this.$input ? this.$input.val() : undefined;
},
format_for_input: function(val) {
return val==null ? "" : val;
},
validate: function(v) {
if(this.df.options == 'Phone') {
if(v+''=='') {
return '';
}
var v1 = '';
// phone may start with + and must only have numbers later, '-' and ' ' are stripped
v = v.replace(/ /g, '').replace(/-/g, '').replace(/\(/g, '').replace(/\)/g, '');

// allow initial +,0,00
if(v && v.substr(0,1)=='+') {
v1 = '+'; v = v.substr(1);
}
if(v && v.substr(0,2)=='00') {
v1 += '00'; v = v.substr(2);
}
if(v && v.substr(0,1)=='0') {
v1 += '0'; v = v.substr(1);
}
v1 += cint(v) + '';
return v1;
} else if(this.df.options == 'Email') {
if(v+''=='') {
return '';
}

var email_list = frappe.utils.split_emails(v);
if (!email_list) {
// invalid email
return '';
} else {
var invalid_email = false;
email_list.forEach(function(email) {
if (!validate_email(email)) {
frappe.msgprint(__("Invalid Email: {0}", [email]));
invalid_email = true;
}
});

if (invalid_email) {
// at least 1 invalid email
return '';
} else {
// all good
return v;
}
}

} else {
return v;
}
}
});

+ 106
- 0
frappe/public/js/frappe/form/controls/date.js Ver arquivo

@@ -0,0 +1,106 @@
frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({
make_input: function() {
this._super();
this.set_date_options();
this.set_datepicker();
this.set_t_for_today();
},
set_formatted_input: function(value) {
this._super(value);
if(!value) return;

let should_refresh = this.last_value && this.last_value !== value;

if (!should_refresh) {
if(this.datepicker.selectedDates.length > 0) {
// if date is selected but different from value, refresh
const selected_date =
moment(this.datepicker.selectedDates[0])
.format(moment.defaultDateFormat);
should_refresh = selected_date !== value;
} else {
// if datepicker has no selected date, refresh
should_refresh = true;
}
}

if(should_refresh) {
this.datepicker.selectDate(frappe.datetime.str_to_obj(value));
}
},
set_date_options: function() {
var me = this;
var lang = frappe.boot.user.language;
if(!$.fn.datepicker.language[lang]) {
lang = 'en';
}
this.today_text = __("Today");
this.datepicker_options = {
language: lang,
autoClose: true,
todayButton: frappe.datetime.now_date(true),
dateFormat: (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'),
startDate: frappe.datetime.now_date(true),
onSelect: () => {
this.$input.trigger('change');
},
onShow: () => {
this.datepicker.$datepicker
.find('.datepicker--button:visible')
.text(me.today_text);

this.update_datepicker_position();
}
};
},
update_datepicker_position: function() {
if(!this.frm) return;
// show datepicker above or below the input
// based on scroll position
var window_height = $(window).height();
var window_scroll_top = $(window).scrollTop();
var el_offset_top = this.$input.offset().top + 280;
var position = 'top left';
if(window_height + window_scroll_top >= el_offset_top) {
position = 'bottom left';
}
this.datepicker.update('position', position);
},
set_datepicker: function() {
this.$input.datepicker(this.datepicker_options);
this.datepicker = this.$input.data('datepicker');
},
set_t_for_today: function() {
var me = this;
this.$input.on("keydown", function(e) {
if(e.which===84) { // 84 === t
if(me.df.fieldtype=='Date') {
me.set_value(frappe.datetime.nowdate());
} if(me.df.fieldtype=='Datetime') {
me.set_value(frappe.datetime.now_datetime());
} if(me.df.fieldtype=='Time') {
me.set_value(frappe.datetime.now_time());
}
return false;
}
});
},
parse: function(value) {
if(value) {
return frappe.datetime.user_to_str(value);
}
},
format_for_input: function(value) {
if(value) {
return frappe.datetime.str_to_user(value);
}
return "";
},
validate: function(value) {
if(value && !frappe.datetime.validate(value)) {
frappe.msgprint(__("Date must be in format: {0}", [frappe.sys_defaults.date_format || "yyyy-mm-dd"]));
return '';
}
return value;
}
});

+ 62
- 0
frappe/public/js/frappe/form/controls/date_range.js Ver arquivo

@@ -0,0 +1,62 @@
frappe.ui.form.ControlDateRange = frappe.ui.form.ControlData.extend({
make_input: function() {
this._super();
this.set_date_options();
this.set_datepicker();
this.refresh();
},
set_date_options: function() {
var me = this;
this.datepicker_options = {
language: "en",
range: true,
autoClose: true,
toggleSelected: false
};
this.datepicker_options.dateFormat =
(frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd');
this.datepicker_options.onSelect = function() {
me.$input.trigger('change');
};
},
set_datepicker: function() {
this.$input.datepicker(this.datepicker_options);
this.datepicker = this.$input.data('datepicker');
},
set_input: function(value, value2) {
this.last_value = this.value;
if (value && value2) {
this.value = [value, value2];
} else {
this.value = value;
}
if (this.value) {
let formatted = this.format_for_input(this.value[0], this.value[1]);
this.$input && this.$input.val(formatted);
} else {
this.$input && this.$input.val("");
}
this.set_disp_area(value || '');
this.set_mandatory && this.set_mandatory(value);
},
parse: function(value) {
// replace the separator (which can be in user language) with comma
const to = __('{0} to {1}').replace('{0}', '').replace('{1}', '');
value = value.replace(to, ',');

if(value && value.includes(',')) {
var vals = value.split(',');
var from_date = moment(frappe.datetime.user_to_obj(vals[0])).format('YYYY-MM-DD');
var to_date = moment(frappe.datetime.user_to_obj(vals[vals.length-1])).format('YYYY-MM-DD');
return [from_date, to_date];
}
},
format_for_input: function(value1, value2) {
if(value1 && value2) {
value1 = frappe.datetime.str_to_user(value1);
value2 = frappe.datetime.str_to_user(value2);
return __("{0} to {1}").format([value1, value2]);
}
return "";
}
});

+ 23
- 0
frappe/public/js/frappe/form/controls/datetime.js Ver arquivo

@@ -0,0 +1,23 @@
frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
set_date_options: function() {
this._super();
this.today_text = __("Now");
$.extend(this.datepicker_options, {
timepicker: true,
timeFormat: "hh:ii:ss",
todayButton: frappe.datetime.now_datetime(true)
});
},
set_description: function() {
const { description } = this.df;
const { time_zone } = frappe.sys_defaults;
if (!frappe.datetime.is_timezone_same()) {
if (!description) {
this.df.description = time_zone;
} else if (!description.includes(time_zone)) {
this.df.description += '<br>' + time_zone;
}
}
this._super();
}
});

+ 21
- 0
frappe/public/js/frappe/form/controls/dynamic_link.js Ver arquivo

@@ -0,0 +1,21 @@
frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({
get_options: function() {
if(this.df.get_options) {
return this.df.get_options();
}
if (this.docname==null && cur_dialog) {
//for dialog box
return cur_dialog.get_value(this.df.options);
}
if (cur_frm==null && cur_list){
//for list page
return cur_list.wrapper.find("input[data-fieldname*="+this.df.options+"]").val();
}
var options = frappe.model.get_value(this.df.parent, this.docname, this.df.options);
// if(!options) {
// frappe.msgprint(__("Please set {0} first",
// [frappe.meta.get_docfield(this.df.parent, this.df.options, this.docname).label]));
// }
return options;
},
});

+ 27
- 0
frappe/public/js/frappe/form/controls/float.js Ver arquivo

@@ -0,0 +1,27 @@
frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
parse: function(value) {
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());
},

format_for_input: function(value) {
var number_format;
if (this.df.fieldtype==="Float" && this.df.options && this.df.options.trim()) {
number_format = this.get_number_format();
}
var formatted_value = format_number(parseFloat(value), number_format, this.get_precision());
return isNaN(parseFloat(value)) ? "" : formatted_value;
},

// even a float field can be formatted based on currency format instead of float format
get_number_format: function() {
var currency = frappe.meta.get_field_currency(this.df, this.get_doc());
return get_number_format(currency);
},

get_precision: function() {
// round based on field precision or float precision, else don't round
return this.df.precision || cint(frappe.boot.sysdefaults.float_precision, null);
}
});

frappe.ui.form.ControlPercent = frappe.ui.form.ControlFloat;

+ 5
- 0
frappe/public/js/frappe/form/controls/heading.js Ver arquivo

@@ -0,0 +1,5 @@
frappe.ui.form.ControlHeading = frappe.ui.form.ControlHTML.extend({
get_content: function() {
return "<h4>" + __(this.df.label) + "</h4>";
}
});

+ 26
- 0
frappe/public/js/frappe/form/controls/html.js Ver arquivo

@@ -0,0 +1,26 @@
frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({
make: function() {
this._super();
this.disp_area = this.wrapper;
},
refresh_input: function() {
var content = this.get_content();
if(content) this.$wrapper.html(content);
},
get_content: function() {
return this.df.options || "";
},
html: function(html) {
this.$wrapper.html(html || this.get_content());
},
set_value: function(html) {
if(html.appendTo) {
// jquery object
html.appendTo(this.$wrapper.empty());
} else {
// html
this.df.options = html;
this.html(html);
}
}
});

+ 22
- 0
frappe/public/js/frappe/form/controls/image.js Ver arquivo

@@ -0,0 +1,22 @@
frappe.ui.form.ControlImage = frappe.ui.form.Control.extend({
make: function() {
this._super();
this.$wrapper.css({"margin": "0px"});
this.$body = $("<div></div>").appendTo(this.$wrapper)
.css({"margin-bottom": "10px"});
$('<div class="clearfix"></div>').appendTo(this.$wrapper);
},
refresh_input: function() {
this.$body.empty();

var doc = this.get_doc();
if(doc && this.df.options && doc[this.df.options]) {
this.$img = $("<img src='"+doc[this.df.options]+"' class='img-responsive'>")
.appendTo(this.$body);
} else {
this.$buffer = $("<div class='missing-image'><i class='octicon octicon-circle-slash'></i></div>")
.appendTo(this.$body);
}
return false;
}
});

+ 25
- 0
frappe/public/js/frappe/form/controls/int.js Ver arquivo

@@ -0,0 +1,25 @@
frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({
make: function() {
this._super();
// $(this.label_area).addClass('pull-right');
// $(this.disp_area).addClass('text-right');
},
make_input: function() {
var me = this;
this._super();
this.$input
// .addClass("text-right")
.on("focus", function() {
setTimeout(function() {
if(!document.activeElement) return;
document.activeElement.value
= me.validate(document.activeElement.value);
document.activeElement.select();
}, 100);
return false;
});
},
parse: function(value) {
return cint(value, null);
}
});

+ 382
- 0
frappe/public/js/frappe/form/controls/link.js Ver arquivo

@@ -0,0 +1,382 @@
// special features for link
// buttons
// autocomplete
// link validation
// custom queries
// add_fetches
frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
make_input: function() {
var me = this;
// line-height: 1 is for Mozilla 51, shows extra padding otherwise
$('<div class="link-field ui-front" style="position: relative; line-height: 1;">\
<input type="text" class="input-with-feedback form-control">\
<span class="link-btn">\
<a class="btn-open no-decoration" title="' + __("Open Link") + '">\
<i class="octicon octicon-arrow-right"></i></a>\
</span>\
</div>').prependTo(this.input_area);
this.$input_area = $(this.input_area);
this.$input = this.$input_area.find('input');
this.$link = this.$input_area.find('.link-btn');
this.$link_open = this.$link.find('.btn-open');
this.set_input_attributes();
this.$input.on("focus", function() {
setTimeout(function() {
if(me.$input.val() && me.get_options()) {
me.$link.toggle(true);
me.$link_open.attr('href', '#Form/' + me.get_options() + '/' + me.$input.val());
}

if(!me.$input.val()) {
me.$input.val("").trigger("input");
}
}, 500);
});
this.$input.on("blur", function() {
// if this disappears immediately, the user's click
// does not register, hence timeout
setTimeout(function() {
me.$link.toggle(false);
}, 500);
});
this.input = this.$input.get(0);
this.has_input = true;
this.translate_values = true;
this.setup_buttons();
this.setup_awesomeplete();
if(this.df.change) {
this.$input.on("change", function() {
me.df.change.apply(this);
});
}
},
get_options: function() {
return this.df.options;
},
setup_buttons: function() {
if(this.only_input && !this.with_link_btn) {
this.$input_area.find(".link-btn").remove();
}
},
open_advanced_search: function() {
var doctype = this.get_options();
if(!doctype) return;
new frappe.ui.form.LinkSelector({
doctype: doctype,
target: this,
txt: this.get_input_value()
});
return false;
},
new_doc: function() {
var doctype = this.get_options();
var me = this;

if(!doctype) return;

// set values to fill in the new document
if(this.df.get_route_options_for_new_doc) {
frappe.route_options = this.df.get_route_options_for_new_doc(this);
} else {
frappe.route_options = {};
}

// partially entered name field
frappe.route_options.name_field = this.get_value();

// reference to calling link
frappe._from_link = this;
frappe._from_link_scrollY = $(document).scrollTop();

frappe.ui.form.make_quick_entry(doctype, (doc) => {
return me.set_value(doc.name);
});

return false;
},
setup_awesomeplete: function() {
var me = this;

this.$input.cache = {};

this.awesomplete = new Awesomplete(me.input, {
minChars: 0,
maxItems: 99,
autoFirst: true,
list: [],
data: function (item) {
return {
label: item.label || item.value,
value: item.value
};
},
filter: function() {
return true;
},
item: function (item) {
var d = this.get_item(item.value);
if(!d.label) { d.label = d.value; }

var _label = (me.translate_values) ? __(d.label) : d.label;
var html = "<strong>" + _label + "</strong>";
if(d.description && d.value!==d.description) {
html += '<br><span class="small">' + __(d.description) + '</span>';
}
return $('<li></li>')
.data('item.autocomplete', d)
.prop('aria-selected', 'false')
.html('<a><p>' + html + '</p></a>')
.get(0);
},
sort: function() {
return 0;
}
});

this.$input.on("input", function(e) {
var doctype = me.get_options();
if(!doctype) return;
if (!me.$input.cache[doctype]) {
me.$input.cache[doctype] = {};
}

var term = e.target.value;

if (me.$input.cache[doctype][term]!=null) {
// immediately show from cache
me.awesomplete.list = me.$input.cache[doctype][term];
}

var args = {
'txt': term,
'doctype': doctype,
};

me.set_custom_query(args);

frappe.call({
type: "GET",
method:'frappe.desk.search.search_link',
no_spinner: true,
args: args,
callback: function(r) {
if(!me.$input.is(":focus")) {
return;
}

if(!me.df.only_select) {
if(frappe.model.can_create(doctype)
&& me.df.fieldtype !== "Dynamic Link") {
// new item
r.results.push({
label: "<span class='text-primary link-option'>"
+ "<i class='fa fa-plus' style='margin-right: 5px;'></i> "
+ __("Create a new {0}", [__(me.df.options)])
+ "</span>",
value: "create_new__link_option",
action: me.new_doc
});
}
// advanced search
r.results.push({
label: "<span class='text-primary link-option'>"
+ "<i class='fa fa-search' style='margin-right: 5px;'></i> "
+ __("Advanced Search")
+ "</span>",
value: "advanced_search__link_option",
action: me.open_advanced_search
});
}
me.$input.cache[doctype][term] = r.results;
me.awesomplete.list = me.$input.cache[doctype][term];
}
});
});

this.$input.on("blur", function() {
if(me.selected) {
me.selected = false;
return;
}
var value = me.get_input_value();
if(value!==me.last_value) {
me.parse_validate_and_set_in_model(value);
}
});

this.$input.on("awesomplete-open", function() {
me.$wrapper.css({"z-index": 100});
me.$wrapper.find('ul').css({"z-index": 100});
me.autocomplete_open = true;
});

this.$input.on("awesomplete-close", function() {
me.$wrapper.css({"z-index": 1});
me.autocomplete_open = false;
});

this.$input.on("awesomplete-select", function(e) {
var o = e.originalEvent;
var item = me.awesomplete.get_item(o.text.value);

me.autocomplete_open = false;

// prevent selection on tab
var TABKEY = 9;
if(e.keyCode === TABKEY) {
e.preventDefault();
me.awesomplete.close();
return false;
}

if(item.action) {
item.value = "";
item.action.apply(me);
}

// if remember_last_selected is checked in the doctype against the field,
// then add this value
// to defaults so you do not need to set it again
// unless it is changed.
if(me.df.remember_last_selected_value) {
frappe.boot.user.last_selected_values[me.df.options] = item.value;
}

me.parse_validate_and_set_in_model(item.value);
});

this.$input.on("awesomplete-selectcomplete", function(e) {
var o = e.originalEvent;
if(o.text.value.indexOf("__link_option") !== -1) {
me.$input.val("");
}
});
},
set_custom_query: function(args) {
var set_nulls = function(obj) {
$.each(obj, function(key, value) {
if(value!==undefined) {
obj[key] = value;
}
});
return obj;
};
if(this.get_query || this.df.get_query) {
var get_query = this.get_query || this.df.get_query;
if($.isPlainObject(get_query)) {
var filters = null;
if(get_query.filters) {
// passed as {'filters': {'key':'value'}}
filters = get_query.filters;
} else if(get_query.query) {

// passed as {'query': 'path.to.method'}
args.query = get_query;
} else {

// dict is filters
filters = get_query;
}

if (filters) {
filters = set_nulls(filters);

// extend args for custom functions
$.extend(args, filters);

// add "filters" for standard query (search.py)
args.filters = filters;
}
} else if(typeof(get_query)==="string") {
args.query = get_query;
} else {
// get_query by function
var q = (get_query)(this.frm && this.frm.doc || this.doc, this.doctype, this.docname);

if (typeof(q)==="string") {
// returns a string
args.query = q;
} else if($.isPlainObject(q)) {
// returns a plain object with filters
if(q.filters) {
set_nulls(q.filters);
}

// turn off value translation
if(q.translate_values !== undefined) {
this.translate_values = q.translate_values;
}

// extend args for custom functions
$.extend(args, q);

// add "filters" for standard query (search.py)
args.filters = q.filters;
}
}
}
if(this.df.filters) {
set_nulls(this.df.filters);
if(!args.filters) args.filters = {};
$.extend(args.filters, this.df.filters);
}
},
validate: function(value) {
// validate the value just entered
if(this.df.options=="[Select]" || this.df.ignore_link_validation) {
return value;
}

return this.validate_link_and_fetch(this.df, this.get_options(),
this.docname, value);
},
validate_link_and_fetch: function(df, doctype, docname, value) {
var me = this;

if(value) {
return new Promise((resolve) => {
var fetch = '';

if(this.frm && this.frm.fetch_dict[df.fieldname]) {
fetch = this.frm.fetch_dict[df.fieldname].columns.join(', ');
}

return frappe.call({
method:'frappe.desk.form.utils.validate_link',
type: "GET",
args: {
'value': value,
'options': doctype,
'fetch': fetch
},
no_spinner: true,
callback: function(r) {
if(r.message=='Ok') {
if(r.fetch_values && docname) {
me.set_fetch_values(df, docname, r.fetch_values);
}
resolve(r.valid_value);
} else {
resolve("");
}
}
});
});
}
},
set_fetch_values: function(df, docname, fetch_values) {
var fl = this.frm.fetch_dict[df.fieldname].fields;
for(var i=0; i < fl.length; i++) {
frappe.model.set_value(df.parent, docname, fl[i], fetch_values[i], df.fieldtype);
}
}
});

if(Awesomplete) {
Awesomplete.prototype.get_item = function(value) {
return this._list.find(function(item) {
return item.value === value;
});
};
}


+ 52
- 0
frappe/public/js/frappe/form/controls/password.js Ver arquivo

@@ -0,0 +1,52 @@
frappe.ui.form.ControlPassword = frappe.ui.form.ControlData.extend({
input_type: "password",
make: function() {
this._super();
},
make_input: function() {
var me = this;
this._super();
this.$input.parent().append($('<span class="password-strength-indicator indicator"></span>'));
this.$wrapper.find('.control-input-wrapper').append($('<p class="password-strength-message text-muted small hidden"></p>'));

this.indicator = this.$wrapper.find('.password-strength-indicator');
this.message = this.$wrapper.find('.help-box');

this.$input.on('input', () => {
var $this = $(this);
clearTimeout($this.data('timeout'));
$this.data('timeout', setTimeout(() => {
var txt = me.$input.val();
me.get_password_strength(txt);
}), 300);
});
},
get_password_strength: function(value) {
var me = this;
frappe.call({
type: 'GET',
method: 'frappe.core.doctype.user.user.test_password_strength',
args: {
new_password: value || ''
},
callback: function(r) {
if (r.message && r.message.entropy) {
var score = r.message.score,
feedback = r.message.feedback;

feedback.crack_time_display = r.message.crack_time_display;

var indicators = ['grey', 'red', 'orange', 'yellow', 'green'];
me.set_strength_indicator(indicators[score]);

}
}

});
},
set_strength_indicator: function(color) {
var message = __("Include symbols, numbers and capital letters in the password");
this.indicator.removeClass().addClass('password-strength-indicator indicator ' + color);
this.message.html(message).removeClass('hidden');
}
});

+ 8
- 0
frappe/public/js/frappe/form/controls/read_only.js Ver arquivo

@@ -0,0 +1,8 @@
frappe.ui.form.ControlReadOnly = frappe.ui.form.ControlData.extend({
get_status: function(explain) {
var status = this._super(explain);
if(status==="Write")
status = "Read";
return;
},
});

+ 64
- 0
frappe/public/js/frappe/form/controls/select.js Ver arquivo

@@ -0,0 +1,64 @@
frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
html_element: "select",
make_input: function() {
this._super();
this.set_options();
},
set_formatted_input: function(value) {
// refresh options first - (new ones??)
if(value==null) value = '';
this.set_options(value);

// set in the input element
this._super(value);

// check if the value to be set is selected
var input_value = '';
if(this.$input) {
input_value = this.$input.val();
}

if(value && input_value && value !== input_value) {
// trying to set a non-existant value
// model value must be same as whatever the input is
this.set_model_value(input_value);
}
},
set_options: function(value) {
// reset options, if something new is set
var options = this.df.options || [];
if(typeof this.df.options==="string") {
options = this.df.options.split("\n");
}

// nothing changed
if(options.toString() === this.last_options) {
return;
}
this.last_options = options.toString();

if(this.$input) {
var selected = this.$input.find(":selected").val();
this.$input.empty().add_options(options || []);

if(value===undefined && selected) {
this.$input.val(selected);
}
}
},
get_file_attachment_list: function() {
if(!this.frm) return;
var fl = frappe.model.docinfo[this.frm.doctype][this.frm.docname];
if(fl && fl.attachments) {
this.set_description("");
var options = [""];
$.each(fl.attachments, function(i, f) {
options.push(f.file_url);
});
return options;
} else {
this.set_description(__("Please attach a file first."));
return [""];
}
}
});

+ 120
- 0
frappe/public/js/frappe/form/controls/signature.js Ver arquivo

@@ -0,0 +1,120 @@
frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({
saving: false,
loading: false,
make: function() {
var me = this;
this._super();

// make jSignature field
this.body = $('<div class="signature-field"></div>').appendTo(me.wrapper);
this.make_pad();

this.img_wrapper = $(`<div class="signature-display">
<div class="missing-image attach-missing-image">
<i class="octicon octicon-circle-slash"></i>
</div></div>`)
.appendTo(this.wrapper);
this.img = $("<img class='img-responsive attach-image-display'>")
.appendTo(this.img_wrapper).toggle(false);

},
make_pad: function() {
let width = this.body.width();
if (width > 0 && !this.$pad) {
this.$pad = this.body.jSignature({
height: 300,
width: this.body.width(),
lineWidth: 0.8
}).on('change',
this.on_save_sign.bind(this));
this.load_pad();
this.$reset_button_wrapper = $(`<div class="signature-btn-row">
<a href="#" type="button" class="signature-reset btn btn-default">
<i class="glyphicon glyphicon-repeat"></i></a>`)
.appendTo(this.$pad)
.on("click", '.signature-reset', () => {
this.on_reset_sign();
return false;
});

}
},
refresh_input: function(e) {
// prevent to load the second time
this.make_pad();
this.$wrapper.find(".control-input").toggle(false);
this.set_editable(this.get_status()=="Write");
this.load_pad();
if(this.get_status()=="Read") {
$(this.disp_area).toggle(false);
}
},
set_image: function(value) {
if(value) {
$(this.img_wrapper).find(".missing-image").toggle(false);
this.img.attr("src", value).toggle(true);
} else {
$(this.img_wrapper).find(".missing-image").toggle(true);
this.img.toggle(false);
}
},
load_pad: function() {
// make sure not triggered during saving
if (this.saving) return;
// get value
var value = this.get_value();
// import data for pad
if (this.$pad) {
this.loading = true;
// reset in all cases
this.$pad.jSignature('reset');
if (value) {
// load the image to find out the size, because scaling will affect
// stroke width
try {
this.$pad.jSignature('setData', value);
this.set_image(value);
}
catch (e){
console.log("Cannot set data for signature", value, e);
}
}

this.loading = false;
}
},
set_editable: function(editable) {
this.$pad && this.$pad.toggle(editable);
this.img_wrapper.toggle(!editable);
if (this.$reset_button_wrapper) {
this.$reset_button_wrapper.toggle(editable);
if (editable) {
this.$reset_button_wrapper.addClass('editing');
}
else {
this.$reset_button_wrapper.removeClass('editing');
}
}
},
set_my_value: function(value) {
if (this.saving || this.loading) return;
this.saving = true;
this.set_value(value);
this.saving = false;
},
get_value: function() {
return this.value ? this.value: this.get_model_value();
},
// reset signature canvas
on_reset_sign: function() {
this.$pad.jSignature("reset");
this.set_my_value("");
},
// save signature value to model and display
on_save_sign: function() {
if (this.saving || this.loading) return;
var base64_img = this.$pad.jSignature("getData");
this.set_my_value(base64_img);
this.set_image(this.get_value());
}
});

+ 30
- 0
frappe/public/js/frappe/form/controls/table.js Ver arquivo

@@ -0,0 +1,30 @@
frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
make: function() {
this._super();

// add title if prev field is not column / section heading or html
this.grid = new frappe.ui.form.Grid({
frm: this.frm,
df: this.df,
perm: this.perm || (this.frm && this.frm.perm) || this.df.perm,
parent: this.wrapper
});
if(this.frm) {
this.frm.grids[this.frm.grids.length] = this;
}

// description
if(this.df.description) {
$('<p class="text-muted small">' + __(this.df.description) + '</p>')
.appendTo(this.wrapper);
}
},
refresh_input: function() {
this.grid.refresh();
},
get_value: function() {
if(this.grid) {
return this.grid.get_data();
}
}
});

+ 20
- 0
frappe/public/js/frappe/form/controls/text.js Ver arquivo

@@ -0,0 +1,20 @@
frappe.ui.form.ControlText = frappe.ui.form.ControlData.extend({
html_element: "textarea",
horizontal: false,
make_wrapper: function() {
this._super();
this.$wrapper.find(".like-disabled-input").addClass("for-description");
},
make_input: function() {
this._super();
this.$input.css({'height': '300px'});
}
});

frappe.ui.form.ControlLongText = frappe.ui.form.ControlText;
frappe.ui.form.ControlSmallText = frappe.ui.form.ControlText.extend({
make_input: function() {
this._super();
this.$input.css({'height': '150px'});
}
});

+ 297
- 0
frappe/public/js/frappe/form/controls/text_editor.js Ver arquivo

@@ -0,0 +1,297 @@
frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
make_input: function() {
this.has_input = true;
this.make_editor();
this.hide_elements_on_mobile();
this.setup_drag_drop();
this.setup_image_dialog();
this.setting_count = 0;
},
make_editor: function() {
var me = this;
this.editor = $("<div>").appendTo(this.input_area);

// Note: while updating summernote, please make sure all 'p' blocks
// in the summernote source code are replaced by 'div' blocks.
// by default summernote, adds <p> blocks for new paragraphs, which adds
// unexpected whitespaces, esp for email replies.

this.editor.summernote({
minHeight: 400,
toolbar: [
['magic', ['style']],
['style', ['bold', 'italic', 'underline', 'clear']],
['fontsize', ['fontsize']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph', 'hr']],
//['height', ['height']],
['media', ['link', 'picture', 'video', 'table']],
['misc', ['fullscreen', 'codeview']]
],
keyMap: {
pc: {
'CTRL+ENTER': ''
},
mac: {
'CMD+ENTER': ''
}
},
prettifyHtml: true,
dialogsInBody: true,
callbacks: {
onInit: function() {
// firefox hack that puts the caret in the wrong position
// when div is empty. To fix, seed with a <br>.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=550434
// this function is executed only once
$(".note-editable[contenteditable='true']").one('focus', function() {
var $this = $(this);
$this.html($this.html() + '<br>');
});
},
onChange: function(value) {
me.parse_validate_and_set_in_model(value);
},
onKeydown: function(e) {
me._last_change_on = new Date();
var key = frappe.ui.keys.get_key(e);
// prevent 'New DocType (Ctrl + B)' shortcut in editor
if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) {
e.stopPropagation();
}
if(key.indexOf('escape') !== -1) {
if(me.note_editor.hasClass('fullscreen')) {
// exit fullscreen on escape key
me.note_editor
.find('.note-btn.btn-fullscreen')
.trigger('click');
}
}
},
},
icons: {
'align': 'fa fa-align',
'alignCenter': 'fa fa-align-center',
'alignJustify': 'fa fa-align-justify',
'alignLeft': 'fa fa-align-left',
'alignRight': 'fa fa-align-right',
'indent': 'fa fa-indent',
'outdent': 'fa fa-outdent',
'arrowsAlt': 'fa fa-arrows-alt',
'bold': 'fa fa-bold',
'caret': 'caret',
'circle': 'fa fa-circle',
'close': 'fa fa-close',
'code': 'fa fa-code',
'eraser': 'fa fa-eraser',
'font': 'fa fa-font',
'frame': 'fa fa-frame',
'italic': 'fa fa-italic',
'link': 'fa fa-link',
'unlink': 'fa fa-chain-broken',
'magic': 'fa fa-magic',
'menuCheck': 'fa fa-check',
'minus': 'fa fa-minus',
'orderedlist': 'fa fa-list-ol',
'pencil': 'fa fa-pencil',
'picture': 'fa fa-image',
'question': 'fa fa-question',
'redo': 'fa fa-redo',
'square': 'fa fa-square',
'strikethrough': 'fa fa-strikethrough',
'subscript': 'fa fa-subscript',
'superscript': 'fa fa-superscript',
'table': 'fa fa-table',
'textHeight': 'fa fa-text-height',
'trash': 'fa fa-trash',
'underline': 'fa fa-underline',
'undo': 'fa fa-undo',
'unorderedlist': 'fa fa-list-ul',
'video': 'fa fa-video-camera'
}
});
this.note_editor = $(this.input_area).find('.note-editor');
// to fix <p> on enter
//this.set_formatted_input('<div><br></div>');
},
setup_drag_drop: function() {
var me = this;
this.note_editor.on('dragenter dragover', false)
.on('drop', function(e) {
var dataTransfer = e.originalEvent.dataTransfer;

if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
me.note_editor.focus();

var files = [].slice.call(dataTransfer.files);

files.forEach(file => {
me.get_image(file, (url) => {
me.editor.summernote('insertImage', url, file.name);
});
});
}
e.preventDefault();
e.stopPropagation();
});
},
get_image: function (fileobj, callback) {
var freader = new FileReader();

freader.onload = function() {
var dataurl = freader.result;
// add filename to dataurl
var parts = dataurl.split(",");
parts[0] += ";filename=" + fileobj.name;
dataurl = parts[0] + ',' + parts[1];
callback(dataurl);
};
freader.readAsDataURL(fileobj);
},
hide_elements_on_mobile: function() {
this.note_editor.find('.note-btn-underline,\
.note-btn-italic, .note-fontsize,\
.note-color, .note-height, .btn-codeview')
.addClass('hidden-xs');
if($('.toggle-sidebar').is(':visible')) {
// disable tooltips on mobile
this.note_editor.find('.note-btn')
.attr('data-original-title', '');
}
},
get_input_value: function() {
return this.editor? this.editor.summernote('code'): '';
},
parse: function(value) {
if(value == null) value = "";
return frappe.dom.remove_script_and_style(value);
},
set_formatted_input: function(value) {
if(value !== this.get_input_value()) {
this.set_in_editor(value);
}
},
set_in_editor: function(value) {
// set values in editor only if
// 1. value not be set in the last 500ms
// 2. user has not typed anything in the last 3seconds
// ---
// we will attempt to cleanup the user's DOM, hence if this happens
// in the middle of the user is typing, it creates a lot of issues
// also firefox tends to reset the cursor for some reason if the values
// are reset

if(this.setting_count > 2) {
// we don't understand how the internal triggers work,
// so if someone is setting the value third time, then quit
return;
}

this.setting_count += 1;

let time_since_last_keystroke = moment() - moment(this._last_change_on);

if(!this._last_change_on || (time_since_last_keystroke > 3000)) {
setTimeout(() => this.setting_count = 0, 500);
this.editor.summernote('code', value || '');
} else {
this._setting_value = setInterval(() => {
if(time_since_last_keystroke > 3000) {
if(this.last_value !== this.get_input_value()) {
// if not already in sync, reset
this.editor.summernote('code', this.last_value || '');
}
clearInterval(this._setting_value);
this._setting_value = null;
this.setting_count = 0;
}
}, 1000);
}
},
set_focus: function() {
return this.editor.summernote('focus');
},
set_upload_options: function() {
var me = this;
this.upload_options = {
parent: this.image_dialog.get_field("upload_area").$wrapper,
args: {},
max_width: this.df.max_width,
max_height: this.df.max_height,
options: "Image",
btn: this.image_dialog.set_primary_action(__("Insert")),
on_no_attach: function() {
// if no attachmemts,
// check if something is selected
var selected = me.image_dialog.get_field("select").get_value();
if(selected) {
me.editor.summernote('insertImage', selected);
me.image_dialog.hide();
} else {
frappe.msgprint(__("Please attach a file or set a URL"));
}
},
callback: function(attachment) {
me.editor.summernote('insertImage', attachment.file_url, attachment.file_name);
me.image_dialog.hide();
},
onerror: function() {
me.image_dialog.hide();
}
};

if ("is_private" in this.df) {
this.upload_options.is_private = this.df.is_private;
}

if(this.frm) {
this.upload_options.args = {
from_form: 1,
doctype: this.frm.doctype,
docname: this.frm.docname
};
} else {
this.upload_options.on_attach = function(fileobj, dataurl) {
me.editor.summernote('insertImage', dataurl);
me.image_dialog.hide();
frappe.hide_progress();
};
}
},

setup_image_dialog: function() {
this.note_editor.find('[data-original-title="Image"]').on('click', () => {
if(!this.image_dialog) {
this.image_dialog = new frappe.ui.Dialog({
title: __("Image"),
fields: [
{fieldtype:"HTML", fieldname:"upload_area"},
{fieldtype:"HTML", fieldname:"or_attach", options: __("Or")},
{fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") },
]
});
}

this.image_dialog.show();
this.image_dialog.get_field("upload_area").$wrapper.empty();

// select from existing attachments
var attachments = this.frm && this.frm.attachments.get_attachments() || [];
var select = this.image_dialog.get_field("select");
if(attachments.length) {
attachments = $.map(attachments, function(o) { return o.file_url; });
select.df.options = [""].concat(attachments);
select.toggle(true);
this.image_dialog.get_field("or_attach").toggle(true);
select.refresh();
} else {
this.image_dialog.get_field("or_attach").toggle(false);
select.toggle(false);
}
select.$input.val("");

this.set_upload_options();
frappe.upload.make(this.upload_options);
});
}
});

+ 44
- 0
frappe/public/js/frappe/form/controls/time.js Ver arquivo

@@ -0,0 +1,44 @@
frappe.ui.form.ControlTime = frappe.ui.form.ControlData.extend({
make_input: function() {
var me = this;
this._super();
this.$input.datepicker({
language: "en",
timepicker: true,
onlyTimepicker: true,
timeFormat: "hh:ii:ss",
startDate: frappe.datetime.now_time(true),
onSelect: function() {
me.$input.trigger('change');
},
onShow: function() {
$('.datepicker--button:visible').text(__('Now'));
},
todayButton: frappe.datetime.now_time(true)
});
this.datepicker = this.$input.data('datepicker');
this.refresh();
},
set_input: function(value) {
this._super(value);
if(value
&& ((this.last_value && this.last_value !== this.value)
|| (!this.datepicker.selectedDates.length))) {

var date_obj = frappe.datetime.moment_to_date_obj(moment(value, 'hh:mm:ss'));
this.datepicker.selectDate(date_obj);
}
},
set_description: function() {
const { description } = this.df;
const { time_zone } = frappe.sys_defaults;
if (!frappe.datetime.is_timezone_same()) {
if (!description) {
this.df.description = time_zone;
} else if (!description.includes(time_zone)) {
this.df.description += '<br>' + time_zone;
}
}
this._super();
}
});

+ 3
- 6
frappe/public/js/frappe/form/footer/attachments.js Ver arquivo

@@ -254,6 +254,9 @@ frappe.ui.get_upload_dialog = function(opts){
"reqd" : false,
"filters": {
'related_doctype': opts.args.doctype
},
onchange: function(){
opts.args.gs_template = this.get_value();
}
},
],
@@ -264,12 +267,6 @@ frappe.ui.get_upload_dialog = function(opts){
dialog.show();
var upload_area = $('<div style="padding-bottom: 25px;"></div>').prependTo(dialog.body);

var fd = dialog.fields_dict;

$(fd.gs_template.input).change(function() {
opts.args.gs_template = fd.gs_template.get_value();
});

frappe.upload.make({
parent: upload_area,
args: opts.args,


+ 1
- 1
frappe/public/js/frappe/form/footer/timeline.js Ver arquivo

@@ -636,7 +636,7 @@ frappe.ui.form.Timeline = Class.extend({
communications = this.frm.get_docinfo().communications,
email = this.get_recipient();

$.each(communications.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }), function(i, c) {
$.each(communications && communications.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }), function(i, c) {
if(c.communication_type=='Communication' && c.communication_medium=="Email") {
if(from_recipient) {
if(c.sender.indexOf(email)!==-1) {


+ 1
- 1
frappe/public/js/frappe/form/footer/timeline_item.html Ver arquivo

@@ -41,7 +41,7 @@
{{ data.user_info.abbr }}</div>
{% } %}
</span>
<div class="asset-details">
<div class="asset-details" data-communication-type = "{{ data.communication_type }}">
<span class="author-wrap">
<i class="{%= data.icon %} hidden-xs fa-fw"></i>
<span title="{%= data.comment_by %}">{%= data.fullname %}</span>


+ 4
- 0
frappe/public/js/frappe/form/save.js Ver arquivo

@@ -28,6 +28,9 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
$(document).trigger("save", [frm.doc]);
callback(r);
},
error: function (r) {
callback(r);
},
btn: btn,
freeze_message: freeze_message
});
@@ -188,6 +191,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
callback: function (r) {
opts.callback && opts.callback(r);
},
error: opts.error,
always: function (r) {
$(btn).prop("disabled", false);
frappe.ui.form.is_saving = false;


+ 7
- 0
frappe/public/js/frappe/form/toolbar.js Ver arquivo

@@ -152,6 +152,13 @@ frappe.ui.form.Toolbar = Class.extend({
this.page.add_menu_item(__("Reload"), function() {
me.frm.reload_doc();}, true);

// add to desktop
if(me.frm.meta.issingle) {
this.page.add_menu_item(__('Add to Desktop'), function () {
frappe.add_to_desktop(me.frm.doctype, me.frm.doctype);
}, true);
}

// delete
if((cint(me.frm.doc.docstatus) != 1) && !me.frm.doc.__islocal
&& frappe.model.can_delete(me.frm.doctype)) {


+ 27
- 3
frappe/public/js/frappe/list/list_renderer.js Ver arquivo

@@ -431,8 +431,9 @@ frappe.views.ListRenderer = Class.extend({
return `<span class='indicator ${indicator[1]}' title='${__(indicator[0])}'></span>`;
},
prepare_data: function (data) {
if (data.modified)
if (data.modified) {
this.prepare_when(data, data.modified);
}

// nulls as strings
for (var key in data) {
@@ -452,8 +453,24 @@ frappe.views.ListRenderer = Class.extend({

var title_field = this.meta.title_field || 'name';
data._title = strip_html(data[title_field] || data.name);
data._full_title = data._title;

// check for duplicates
// add suffix like (1), (2) etc
if (data.name && this.values_map) {
if (this.values_map[data.name]!==undefined) {
if (this.values_map[data.name]===1) {
// update first row!
this.set_title_with_row_number(this.rows_map[data.name], 1);
}
this.values_map[data.name]++;
this.set_title_with_row_number(data, this.values_map[data.name]);
} else {
this.values_map[data.name] = 1;
this.rows_map[data.name] = data;
}
}

data._full_title = data._title;

data._workflow = null;
if (this.workflow_state_fieldname) {
@@ -493,6 +510,11 @@ frappe.views.ListRenderer = Class.extend({
return data;
},

set_title_with_row_number: function (data, id) {
data._title = data._title + ` (${__("Row")} ${id})`;
data._full_title = data._title;
},

prepare_when: function (data, date_str) {
if (!date_str) date_str = data.modified;
// when
@@ -520,10 +542,12 @@ frappe.views.ListRenderer = Class.extend({
|| ($.isArray(this.required_libs) && this.required_libs.length);

this.render_view = function (values) {
me.values_map = {};
me.rows_map = {};
// prepare data before rendering view
values = values.map(me.prepare_data.bind(this));
// remove duplicates
values = values.uniqBy(value => value.name);
// values = values.uniqBy(value => value.name);

if (lib_exists) {
me.load_lib(function () {


+ 1
- 1
frappe/public/js/frappe/misc/tools.js Ver arquivo

@@ -66,7 +66,7 @@ frappe.tools.to_csv = function(data) {
var res = [];
$.each(data, function(i, row) {
row = $.map(row, function(col) {
return typeof(col)==="string" ? ('"' + col.replace(/"/g, '""') + '"') : col;
return typeof(col)==="string" ? ('"' + $('<i>').html(col.replace(/"/g, '""')).text() + '"') : col;
});
res.push(row.join(","));
});


+ 1
- 1
frappe/public/js/frappe/ui/page.js Ver arquivo

@@ -369,7 +369,7 @@ frappe.ui.Page = Class.extend({
.appendTo(this.page_form);
},
add_select: function(label, options) {
var field = this.add_field({label:label, fieldtype:"Select"})
var field = this.add_field({label:label, fieldtype:"Select"});
return field.$wrapper.find("select").empty().add_options(options);
},
add_data: function(label) {


+ 7
- 1
frappe/public/js/frappe/ui/toolbar/search_utils.js Ver arquivo

@@ -251,7 +251,13 @@ frappe.search.utils = {
var level = me.fuzzy_search(keywords, item);
if(level > 0) {
var module = frappe.modules[item];
if(module._doctype) return;
if (module._doctype) return;

// disallow restricted modules
if (frappe.boot.user.allow_modules &&
!frappe.boot.user.allow_modules.includes(module.module_name)) {
return;
}
var ret = {
type: "Module",
label: __("Open {0}", [me.bolden_match_part(__(item), keywords)]),


+ 9
- 2
frappe/public/js/frappe/views/communication.js Ver arquivo

@@ -63,7 +63,7 @@ frappe.views.CommunicationComposer = Class.extend({
{label:__("Send As Email"), fieldtype:"Check",
fieldname:"send_email"},
{label:__("Send me a copy"), fieldtype:"Check",
fieldname:"send_me_a_copy"},
fieldname:"send_me_a_copy", 'default': frappe.boot.user.send_me_a_copy},
{label:__("Send Read Receipt"), fieldtype:"Check",
fieldname:"send_read_receipt"},
{label:__("Communication Medium"), fieldtype:"Select",
@@ -375,7 +375,14 @@ frappe.views.CommunicationComposer = Class.extend({
$(fields.select_print_format.wrapper).toggle(true);
}

$(fields.send_email.input).prop("checked", true)
$(fields.send_email.input).prop("checked", true);

$(fields.send_me_a_copy.input).on('click', () => {
// update send me a copy (make it sticky)
let val = fields.send_me_a_copy.get_value();
frappe.db.set_value('User', frappe.session.user, 'send_me_a_copy', val);
frappe.boot.user.send_me_a_copy = val;
});

// toggle print format
$(fields.send_email.input).click(function() {


+ 91
- 0
frappe/public/js/frappe/views/reports/print_tree.html Ver arquivo

@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>{{ title }}</title>
<link href="{{ base_url }}/assets/frappe/css/bootstrap.css" rel="stylesheet">
<link type="text/css" rel="stylesheet"
href="{{ base_url }}/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" type="text/css" href="{{ base_url }}/assets/frappe/css/tree.css">
<style>
{{ print_css }}
</style>
<style>
.tree.opened::before,
.tree-node.opened::before,
.tree:last-child::after,
.tree-node:last-child::after {
z-index: 1;
border-left: 1px solid #d1d8dd;
background: none;
}
.tree a,
.tree-link {
text-decoration: none;
cursor: default;
}
.tree.opened > .tree-children > .tree-node > .tree-link::before,
.tree-node.opened > .tree-children > .tree-node > .tree-link::before {
border-top: 1px solid #d1d8dd;
z-index: 1;
background: none;
}
i.fa.fa-fw.fa-folder {
z-index: 2;
position: relative;
}
.tree:last-child::after, .tree-node:last-child::after {
display: none;
}
.tree-node-toolbar {
display: none;
}
i.octicon.octicon-primitive-dot.text-extra-muted {
width: 7px;
height: 7px;
border-radius: 50%;
background: #d1d8dd;
display: inline-block;
position: relative;
z-index: 2;
}

@media (max-width: 767px) {
ul.tree-children {
padding-left: 20px;
}
}
</style>
</head>
<body>
<div class="print-format-gutter">
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if print_settings.letter_head && print_settings.letter_head.footer %}
<div class="letter-head-footer">
{{ print_settings.letter_head.footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ __("Page {0} of {1}", [`<span class="page"></span>`, `<span class="topage"></span>`]) }}
</p>
</div>
{% endif %}

<div class="print-format {% if landscape %} landscape {% endif %}">
{% if print_settings.letter_head %}
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
<div class="letter-head">{{ print_settings.letter_head.header }}</div>
</div>
{% endif %}
<div class="tree opened">
{{ tree }}
</div>
</div>
</div>
</body>
</html>

+ 20
- 1
frappe/public/js/frappe/views/treeview.js Ver arquivo

@@ -281,6 +281,18 @@ frappe.views.TreeView = Class.extend({
}
})
},
print_tree: function() {
if(!frappe.model.can_print(this.doctype)) {
frappe.msgprint(__("You are not allowed to print this report"));
return false;
}
var tree = $(".tree:visible").html();
var me = this;
frappe.ui.get_print_settings(false, function(print_settings) {
var title = __(me.docname || me.doctype);
frappe.render_tree({title: title, tree: tree, print_settings:print_settings});
});
},
set_primary_action: function(){
var me = this;
if (!this.opts.disable_add_node && this.can_create) {
@@ -299,13 +311,20 @@ frappe.views.TreeView = Class.extend({
frappe.set_route('List', me.doctype);
}
},
{
label: __('Print'),
action: function() {
me.print_tree();
}

},
{
label: __('Refresh'),
action: function() {
me.make_tree();
}
},
]
];

if (me.opts.menu_items) {
me.menu_items.push.apply(me.menu_items, me.opts.menu_items)


+ 14
- 6
frappe/public/js/legacy/form.js Ver arquivo

@@ -757,13 +757,18 @@ _f.Frm.prototype.savesubmit = function(btn, callback, on_error) {
frappe.validated = true;
me.script_manager.trigger("before_submit").then(function() {
if(!frappe.validated) {
if(on_error)
if(on_error) {
on_error();
}
return;
}

return me.save('Submit', function(r) {
if(!r.exc) {
if(r.exc) {
if (on_error) {
on_error();
}
} else {
frappe.utils.play_sound("submit");
callback && callback();
me.script_manager.trigger("on_submit");
@@ -780,19 +785,22 @@ _f.Frm.prototype.savecancel = function(btn, callback, on_error) {
frappe.validated = true;
me.script_manager.trigger("before_cancel").then(function() {
if(!frappe.validated) {
if(on_error)
if(on_error) {
on_error();
}
return;
}

var after_cancel = function(r) {
if(!r.exc) {
if(r.exc) {
if (on_error) {
on_error();
}
} else {
frappe.utils.play_sound("cancel");
me.refresh();
callback && callback();
me.script_manager.trigger("after_cancel");
} else {
on_error();
}
};
frappe.ui.form.save(me, "cancel", after_cancel, btn);


+ 4
- 4
frappe/public/js/lib/fullcalendar/fullcalendar.min.css
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 8
- 7
frappe/public/js/lib/fullcalendar/fullcalendar.min.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 3
- 3
frappe/public/js/lib/fullcalendar/fullcalendar.print.css Ver arquivo

@@ -1,7 +1,7 @@
/*!
* FullCalendar v3.0.1 Print Stylesheet
* Docs & License: http://fullcalendar.io/
* (c) 2016 Adam Shaw
* FullCalendar v3.4.0 Print Stylesheet
* Docs & License: https://fullcalendar.io/
* (c) 2017 Adam Shaw
*/

/*


+ 7
- 6
frappe/public/js/lib/fullcalendar/gcal.js Ver arquivo

@@ -1,9 +1,10 @@
/*!
* FullCalendar v3.0.1 Google Calendar Plugin
* Docs & License: http://fullcalendar.io/
* (c) 2016 Adam Shaw
* FullCalendar v3.4.0 Google Calendar Plugin
* Docs & License: https://fullcalendar.io/
* (c) 2017 Adam Shaw
*/


(function(factory) {
if (typeof define === 'function' && define.amd) {
define([ 'jquery' ], factory);
@@ -73,7 +74,7 @@ FC.sourceFetchers.push(function(sourceOptions, start, end, timezone) {

function transformOptions(sourceOptions, start, end, timezone, calendar) {
var url = API_BASE + '/' + encodeURIComponent(sourceOptions.googleCalendarId) + '/events?callback=?'; // jsonp
var apiKey = sourceOptions.googleCalendarApiKey || calendar.options.googleCalendarApiKey;
var apiKey = sourceOptions.googleCalendarApiKey || calendar.opt('googleCalendarApiKey');
var success = sourceOptions.success;
var data;
var timezoneArg; // populated when a specific timezone. escaped to Google's liking
@@ -83,7 +84,7 @@ function transformOptions(sourceOptions, start, end, timezone, calendar) {

// call error handlers
(sourceOptions.googleCalendarError || $.noop).apply(calendar, errorObjs);
(calendar.options.googleCalendarError || $.noop).apply(calendar, errorObjs);
(calendar.opt('googleCalendarError') || $.noop).apply(calendar, errorObjs);

// print error to debug console
FC.warn.apply(null, [ message ].concat(apiErrorObjs || []));


+ 5
- 5
frappe/public/js/lib/fullcalendar/locale-all.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 14
- 0
frappe/public/js/lib/microtemplate.js Ver arquivo

@@ -122,4 +122,18 @@ frappe.render_grid = function(opts) {

w.document.write(html);
w.document.close();
},
frappe.render_tree = function(opts) {
opts.base_url = frappe.urllib.get_base_url();
opts.landscape = false;
opts.print_css = frappe.boot.print_css;
var tree = frappe.render_template("print_tree", opts);
var w = window.open();

if(!w) {
frappe.msgprint(__("Please enable pop-ups in your browser"))
}

w.document.write(tree);
w.document.close();
}

+ 3
- 7
frappe/public/js/lib/moment/moment-timezone-with-data.min.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 7
- 673
frappe/public/js/lib/moment/moment-with-locales.min.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 2
- 492
frappe/public/js/lib/moment/moment.min.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 7
- 0
frappe/public/less/email.less Ver arquivo

@@ -194,6 +194,13 @@ hr {
}
}

.screenshot {
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
border: 1px solid @border-color;
margin: 8px 0;
max-width: 100%;
}

/* auto email report */
.report-title {
margin-bottom: 20px;


+ 1
- 1
frappe/sessions.py Ver arquivo

@@ -33,7 +33,7 @@ def clear_cache(user=None):
cache = frappe.cache()

groups = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "roles", "home_page", "linked_with",
"defaults", "user_permissions", "home_page", "linked_with",
"desktop_icons", 'portal_menu_items')

if user:


+ 17
- 11
frappe/tests/test_domainification.py Ver arquivo

@@ -7,6 +7,8 @@ from frappe.core.page.permission_manager.permission_manager import get_roles_and
from frappe.desk.doctype.desktop_icon.desktop_icon import (get_desktop_icons, add_user_icon,
clear_desktop_icons_cache)

from frappe.core.doctype.domain_settings.domain_settings import get_active_modules

class TestDomainification(unittest.TestCase):
def setUp(self):
# create test domain
@@ -33,7 +35,7 @@ class TestDomainification(unittest.TestCase):

def remove_from_active_domains(self, domain=None, remove_all=False):
""" remove domain from domain settings """
if not domain:
if not (domain or remove_all):
return

domain_settings = frappe.get_doc("Domain Settings", "Domain Settings")
@@ -90,8 +92,8 @@ class TestDomainification(unittest.TestCase):

# doctype should be hidden in desktop icon, role permissions
results = get_roles_and_doctypes()
self.assertTrue("Test Domainification" in results.get("doctypes"))
self.assertTrue("_Test Role" in results.get("roles"))
self.assertTrue("Test Domainification" in [d.get("value") for d in results.get("doctypes")])
self.assertTrue("_Test Role" in [d.get("value") for d in results.get("roles")])

self.add_active_domain("_Test Domain 2")
test_doctype.restrict_to_domain = "_Test Domain 2"
@@ -101,18 +103,18 @@ class TestDomainification(unittest.TestCase):
test_role.save()

results = get_roles_and_doctypes()
self.assertTrue("Test Domainification" in results.get("doctypes"))
self.assertTrue("_Test Role" in results.get("roles"))
self.assertTrue("Test Domainification" in [d.get("value") for d in results.get("doctypes")])
self.assertTrue("_Test Role" in [d.get("value") for d in results.get("roles")])

self.remove_from_active_domains("_Test Domain 2")
results = get_roles_and_doctypes()

self.assertTrue("Test Domainification" not in results.get("doctypes"))
self.assertTrue("_Test Role" not in results.get("roles"))
self.assertTrue("Test Domainification" not in [d.get("value") for d in results.get("doctypes")])
self.assertTrue("_Test Role" not in [d.get("value") for d in results.get("roles")])

def test_desktop_icon_for_domainification(self):
""" desktop icon should be hidden if doctype's restrict to domain is not in active domains """
test_doctype = self.new_doctype("Test Domainification")
test_doctype.restrict_to_domain = "_Test Domain 2"
test_doctype.insert()
@@ -144,10 +146,14 @@ class TestDomainification(unittest.TestCase):

self.add_active_domain("_Test Domain 2")

modules = frappe.get_active_modules()
modules = get_active_modules()
self.assertTrue("Contacts" in modules)

# doctype should be hidden from the desk
self.remove_from_active_domains("_Test Domain 2")
modules = frappe.get_active_modules()
self.assertTrue("Test Module" not in modules)
modules = get_active_modules()
self.assertTrue("Contacts" not in modules)

test_module_def = frappe.get_doc("Module Def", "Contacts")
test_module_def.restrict_to_domain = ""
test_module_def.save()

+ 2
- 0
frappe/tests/ui/tests.txt Ver arquivo

@@ -11,3 +11,5 @@ frappe/core/doctype/report/test_query_report.js
frappe/tests/ui/test_linked_with.js
frappe/custom/doctype/customize_form/test_customize_form.js
frappe/desk/doctype/event/test_event.js
frappe/workflow/doctype/workflow/tests/test_workflow_create.js
frappe/workflow/doctype/workflow/tests/test_workflow_test.js

+ 9
- 5
frappe/utils/user.py Ver arquivo

@@ -9,6 +9,7 @@ import frappe.share
from frappe.utils import cint
from frappe.boot import get_allowed_reports
from frappe.permissions import get_roles, get_valid_perms
from frappe.core.doctype.domain_settings.domain_settings import get_active_modules

class UserPermissions:
"""
@@ -63,10 +64,13 @@ class UserPermissions:
def build_doctype_map(self):
"""build map of special doctype properties"""

active_domains = frappe.get_active_domains()

self.doctype_map = {}
for r in frappe.db.sql("""select name, in_create, issingle, istable,
read_only, module from tabDocType""", as_dict=1):
self.doctype_map[r['name']] = r
read_only, restrict_to_domain, module from tabDocType""", as_dict=1):
if (not r.restrict_to_domain) or (r.restrict_to_domain in active_domains):
self.doctype_map[r['name']] = r

def build_perm_map(self):
"""build map of permissions at level 0"""
@@ -91,7 +95,7 @@ class UserPermissions:
self.build_perm_map()
user_shared = frappe.share.get_shared_doctypes()
no_list_view_link = []
active_modules = frappe.get_active_modules() or []
active_modules = get_active_modules() or []

for dt in self.doctype_map:
dtp = self.doctype_map[dt]
@@ -193,8 +197,8 @@ class UserPermissions:

def load_user(self):
d = frappe.db.sql("""select email, first_name, last_name, creation,
email_signature, user_type, language, background_image, background_style, mute_sounds
from tabUser where name = %s""", (self.name,), as_dict=1)[0]
email_signature, user_type, language, background_image, background_style,
mute_sounds, send_me_a_copy from tabUser where name = %s""", (self.name,), as_dict=1)[0]

if not self.can_read:
self.build_permissions()


+ 1
- 1
frappe/website/js/website.js Ver arquivo

@@ -12,7 +12,7 @@ $.extend(frappe, {
_assets_loaded: [],
require: function(url) {
if(frappe._assets_loaded.indexOf(url)!==-1) return;
$.ajax({
return $.ajax({
url: url,
async: false,
dataType: "text",


+ 59
- 0
frappe/workflow/doctype/workflow/tests/test_workflow_create.js Ver arquivo

@@ -0,0 +1,59 @@
QUnit.module('setup');

QUnit.test("Test Workflow", function(assert) {
assert.expect(1);
let done = assert.async();

frappe.run_serially([
() => {
return frappe.tests.make('Workflow', [
{workflow_name: "Test User Workflow"},
{document_type: "User"},
{is_active: 1},
{override_status: 1},
{states: [
[
{state: 'Pending'},
{doc_status: 0},
{allow_edit: 'Administrator'}
],
[
{state: 'Approved'},
{doc_status: 1},
{allow_edit: 'Administrator'}
],
[
{state: 'Rejected'},
{doc_status: 2},
{allow_edit: 'Administrator'}
]
]},
{transitions: [
[
{state: 'Pending'},
{action: 'Review'},
{next_state: 'Pending'},
{allowed: 'Administrator'}
],
[
{state: 'Pending'},
{action: 'Approve'},
{next_state: 'Approved'},
{allowed: 'Administrator'}
],
[
{state: 'Approved'},
{action: 'Reject'},
{next_state: 'Rejected'},
{allowed: 'Administrator'}
],
]},
{workflow_state_field: 'workflow_state'}
]);
},
() => frappe.timeout(1),
() => {assert.equal($('.msgprint').text(), "Created Custom Field workflow_state in User", "Workflow created");},
() => frappe.tests.click_button('Close'),
() => done()
]);
});

+ 53
- 0
frappe/workflow/doctype/workflow/tests/test_workflow_test.js Ver arquivo

@@ -0,0 +1,53 @@
QUnit.module('setup');

QUnit.test("Test Workflow", function(assert) {
assert.expect(5);
let done = assert.async();

frappe.run_serially([
() => frappe.set_route('Form', 'User', 'New User 1'),
() => frappe.timeout(1),
() => {
cur_frm.set_value('email', 'test1@testmail.com');
cur_frm.set_value('first_name', 'Test Name');
cur_frm.set_value('send_welcome_email', 0);
cur_frm.save();
},
() => frappe.timeout(2),
() => frappe.tests.click_button('Actions'),
() => frappe.timeout(0.5),
() => {
let review = $(`.dropdown-menu li:contains("Review"):visible`).size();
let approve = $(`.dropdown-menu li:contains("Approve"):visible`).size();
assert.equal(review, 1, "Review Action exists");
assert.equal(approve, 1, "Approve Action exists");
},
() => frappe.tests.click_dropdown_item('Approve'),
() => frappe.timeout(1),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1),
() => {
assert.equal($('.msgprint').text(), "Did not saveInsufficient Permission for User", "Approve action working");
frappe.tests.click_button('Close');
},
() => frappe.timeout(1),
() => {
$('.user-role input:eq(5)').click();
cur_frm.save();
},
() => frappe.timeout(0.5),
() => frappe.tests.click_button('Actions'),
() => frappe.timeout(0.5),
() => {
let reject = $(`.dropdown-menu li:contains("Reject"):visible`).size();
assert.equal(reject, 1, "Review Action exists");
},
() => frappe.tests.click_dropdown_item('Reject'),
() => frappe.timeout(0.5),
() => {
if(frappe.tests.click_button('Close'))
assert.equal(1, 1, "Reject action works");
},
() => done()
]);
});

Carregando…
Cancelar
Salvar