diff --git a/.eslintrc b/.eslintrc index 84cdc6bb85..fb129c477f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -59,7 +59,6 @@ "PhotoSwipeUI_Default": true, "fluxify": true, "io": true, - "c3": true, "__": true, "_p": true, "_f": true, @@ -119,6 +118,8 @@ "getCookies": true, "get_url_arg": true, "QUnit": true, - "JsBarcode": true + "JsBarcode": true, + "L": true, + "Chart": true } } diff --git a/.gitignore b/.gitignore index 436a08414e..bbd843c61b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,8 @@ dist/ build/ frappe/docs/current .vscode -node_modules \ No newline at end of file +node_modules + + +# Not Recommended, but will remove once webpack ready +package-lock.json diff --git a/.travis.yml b/.travis.yml index 38f61ba37b..ccbcec240a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ install: - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ before_script: - - wget http://chromedriver.storage.googleapis.com/2.27/chromedriver_linux64.zip + - wget http://chromedriver.storage.googleapis.com/2.33/chromedriver_linux64.zip - unzip chromedriver_linux64.zip - sudo apt-get install libnss3 - sudo apt-get --only-upgrade install google-chrome-stable diff --git a/README.md b/README.md index b8427fc055..292d6d1e13 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ Full-stack web application framework that uses Python and MariaDB on the server ### Website For details and documentation, see the website - [https://frappe.io](https://frappe.io) ### License diff --git a/frappe/core/doctype/authentication_log/__init__.py b/__init__.py similarity index 100% rename from frappe/core/doctype/authentication_log/__init__.py rename to __init__.py diff --git a/frappe/__init__.py b/frappe/__init__.py index 49367fe855..306591d1c8 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template -__version__ = '9.2.25' +__version__ = '10.0.0' __title__ = "Frappe Framework" local = Local() @@ -311,6 +311,10 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, def clear_messages(): local.message_log = [] +def clear_last_message(): + if len(local.message_log) > 0: + local.message_log = local.message_log[:-1] + def throw(msg, exc=ValidationError, title=None): """Throw execption and show message (`msgprint`). @@ -378,7 +382,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message as_markdown=False, delayed=True, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, attachments=None, content=None, doctype=None, name=None, reply_to=None, - cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, + cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, inline_images=None, template=None, args=None, header=None): """Send email using user's default **Email Account** or global default **Email Account**. @@ -426,7 +430,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message subject=subject, message=message, text_content=text_content, reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, - attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, in_reply_to=in_reply_to, + attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to, send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, inline_images=inline_images, header=header) @@ -972,9 +976,9 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp ps.insert() def import_doc(path, ignore_links=False, ignore_insert=False, insert=False): - """Import a file using Data Import Tool.""" - from frappe.core.page.data_import_tool import data_import_tool - data_import_tool.import_doc(path, ignore_links=ignore_links, ignore_insert=ignore_insert, insert=insert) + """Import a file using Data Import.""" + from frappe.core.doctype.data_import import data_import + data_import.import_doc(path, ignore_links=ignore_links, ignore_insert=ignore_insert, insert=insert) def copy_doc(doc, ignore_no_copy=True): """ No_copy fields also get copied.""" @@ -1362,7 +1366,7 @@ def logger(module=None, with_more_info=True): def log_error(message=None, title=None): '''Log error to Error Log''' - get_doc(dict(doctype='Error Log', error=as_unicode(message or get_traceback()), + return get_doc(dict(doctype='Error Log', error=as_unicode(message or get_traceback()), method=title)).insert(ignore_permissions=True) def get_desk_link(doctype, name): diff --git a/frappe/app.py b/frappe/app.py index b2e19beff0..4c6acc3538 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import os -import MySQLdb from six import iteritems import logging @@ -27,6 +26,12 @@ from frappe.utils.error import make_error_snapshot from frappe.core.doctype.communication.comment import update_comments_in_parent_after_request from frappe import _ +# imports - third-party imports +import pymysql +from pymysql.constants import ER + +# imports - module imports + local_manager = LocalManager([frappe.local]) _site = None @@ -116,8 +121,15 @@ def init_request(request): frappe.local.http_request = frappe.auth.HTTPRequest() def make_form_dict(request): + import json + + if request.content_type == 'application/json': + args = json.loads(request.data) + else: + args = request.form or request.args + frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \ - for k, v in iteritems(request.form or request.args) }) + for k, v in iteritems(args) }) if "_" in frappe.local.form_dict: # _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict @@ -134,11 +146,8 @@ def handle_exception(e): response = frappe.utils.response.report_error(http_status_code) elif (http_status_code==500 - and isinstance(e, MySQLdb.OperationalError) - and e.args[0] in (1205, 1213)): - # 1205 = lock wait timeout - # 1213 = deadlock - # code 409 represents conflict + and isinstance(e, pymysql.InternalError) + and e.args[0] in (ER.LOCK_WAIT_TIMEOUT, ER.LOCK_DEADLOCK)): http_status_code = 508 elif http_status_code==401: diff --git a/frappe/auth.py b/frappe/auth.py index 2e197eb94a..40694bbfef 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -15,7 +15,7 @@ from frappe.sessions import Session, clear_sessions, delete_session from frappe.modules.patch_handler import check_session_stopped from frappe.translate import get_lang_code from frappe.utils.password import check_password -from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log +from frappe.core.doctype.activity_log.activity_log import add_authentication_log from frappe.utils.background_jobs import enqueue from frappe.twofactor import (should_run_2fa, authenticate_for_2factor, confirm_otp_token, get_cached_user_pass) diff --git a/frappe/build.js b/frappe/build.js index c290f789f9..103b66fdaa 100644 --- a/frappe/build.js +++ b/frappe/build.js @@ -20,6 +20,7 @@ const apps = apps_contents.split('\n'); const app_paths = apps.map(app => path_join(apps_path, app, app)) // base_path of each app const assets_path = path_join(sites_path, 'assets'); let build_map = make_build_map(); +let compiled_js_cache = {}; // cache each js file after it is compiled const file_watcher_port = get_conf().file_watcher_port; // command line args @@ -65,11 +66,12 @@ function watch() { io.emit('reload_css', filename); } }); - watch_js(/*function (filename) { - if(socket_connection) { - io.emit('reload_js', filename); - } - }*/); + watch_js(//function (filename) { + // if(socket_connection) { + // io.emit('reload_js', filename); + // } + //} + ); watch_build_json(); }); @@ -82,9 +84,7 @@ function watch() { }); } -function pack(output_path, inputs, minify) { - const output_type = output_path.split('.').pop(); - +function pack(output_path, inputs, minify, file_changed) { let output_txt = ''; for (const file of inputs) { @@ -93,25 +93,18 @@ function pack(output_path, inputs, minify) { continue; } - let file_content = fs.readFileSync(file, 'utf-8'); - - if (file.endsWith('.html') && output_type === 'js') { - file_content = html_to_js_template(file, file_content); + let force_compile = false; + if (file_changed) { + // if file_changed is passed and is equal to file, force_compile it + force_compile = file_changed === file; } - if(file.endsWith('class.js')) { - file_content = minify_js(file_content, file); - } - - if (file.endsWith('.js') && !file.includes('/lib/') && output_type === 'js' && !file.endsWith('class.js')) { - file_content = babelify(file_content, file, minify); - } + let file_content = get_compiled_file(file, output_path, minify, force_compile); if(!minify) { output_txt += `\n/*\n *\t${file}\n */\n` } output_txt += file_content; - output_txt = output_txt.replace(/['"]use strict['"];/, ''); } @@ -127,13 +120,47 @@ function pack(output_path, inputs, minify) { } } +function get_compiled_file(file, output_path, minify, force_compile) { + const output_type = output_path.split('.').pop(); + + let file_content; + + if (force_compile === false) { + // force compile is false + // attempt to get from cache + file_content = compiled_js_cache[file]; + if (file_content) { + return file_content; + } + } + + file_content = fs.readFileSync(file, 'utf-8'); + + if (file.endsWith('.html') && output_type === 'js') { + file_content = html_to_js_template(file, file_content); + } + + if(file.endsWith('class.js')) { + file_content = minify_js(file_content, file); + } + + if (minify && file.endsWith('.js') && !file.includes('/lib/') && output_type === 'js' && !file.endsWith('class.js')) { + file_content = babelify(file_content, file, minify); + } + + compiled_js_cache[file] = file_content; + return file_content; +} + function babelify(content, path, minify) { let presets = ['env']; + var plugins = ['transform-object-rest-spread'] // Minification doesn't work when loading Frappe Desk // Avoid for now, trace the error and come back. try { return babel.transform(content, { presets: presets, + plugins: plugins, comments: false }).code; } catch (e) { @@ -258,16 +285,16 @@ function watch_less(ondirty) { } function watch_js(ondirty) { - const js_paths = app_paths.map(path => path_join(path, 'public', 'js')); - - const to_watch = filter_valid_paths(js_paths); - chokidar.watch(to_watch).on('change', (filename, stats) => { - console.log(filename, 'dirty'); + chokidar.watch([ + path_join(apps_path, '**', '*.js'), + path_join(apps_path, '**', '*.html') + ]).on('change', (filename) => { // build the target js file for which this js/html file is input for (const target in build_map) { const sources = build_map[target]; if (sources.includes(filename)) { - pack(target, sources); + console.log(filename, 'dirty'); + pack(target, sources, null, filename); ondirty && ondirty(target); // break; } diff --git a/frappe/change_log/v10_0_0.md b/frappe/change_log/v10_0_0.md new file mode 100644 index 0000000000..33367a0312 --- /dev/null +++ b/frappe/change_log/v10_0_0.md @@ -0,0 +1,10 @@ +- Enhanced Data Import Tool + - Data Import Tool is now a normal form, you can maintain records for each Data Import. + - Better error handling + - Background processing for large files + +- Frappé now has a github connector + +- Any doctype can have a calendar view + +- Frappé has a new simple, responsive, modern SVG [charts library](https://github.com/frappe/charts), developed by us \ No newline at end of file diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 519491746d..0dbc83848f 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -3,7 +3,6 @@ import click import hashlib, os, sys, compileall import frappe from frappe import _ -from _mysql_exceptions import ProgrammingError from frappe.commands import pass_context, get_site from frappe.commands.scheduler import _is_scheduler_enabled from frappe.limits import update_limits, get_limits @@ -11,6 +10,12 @@ from frappe.installer import update_site_config from frappe.utils import touch_file, get_site_path from six import text_type +# imports - third-party imports +from pymysql.constants import ER + +# imports - module imports +from frappe.exceptions import SQLError + @click.command('new-site') @click.argument('site') @click.option('--db-name', help='Database name') @@ -348,8 +353,8 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path= try: scheduled_backup(ignore_files=False, force=True) - except ProgrammingError as err: - if err[0] == 1146: + except SQLError as err: + if err[0] == ER.NO_SUCH_TABLE: if force: pass else: @@ -400,8 +405,9 @@ def move(dest_dir, site): @click.command('set-admin-password') @click.argument('admin-password') +@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False) @pass_context -def set_admin_password(context, admin_password): +def set_admin_password(context, admin_password, logout_all_sessions=False): "Set Administrator password for a site" import getpass from frappe.utils.password import update_password @@ -414,7 +420,7 @@ def set_admin_password(context, admin_password): admin_password = getpass.getpass("Administrator's password for {0}: ".format(site)) frappe.connect() - update_password('Administrator', admin_password) + update_password(user='Administrator', pwd=admin_password, logout_all_sessions=logout_all_sessions) frappe.db.commit() admin_password = None finally: diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 9b4d40dcd5..ff606d940d 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -15,7 +15,9 @@ def build(make_copy=False, restore = False, verbose=False): import frappe.build import frappe frappe.init('') - frappe.build.bundle(False, make_copy=make_copy, restore = restore, verbose=verbose) + # don't minify in developer_mode for faster builds + no_compress = frappe.local.conf.developer_mode or False + frappe.build.bundle(no_compress, make_copy=make_copy, restore = restore, verbose=verbose) @click.command('watch') def watch(): @@ -162,12 +164,12 @@ def export_doc(context, doctype, docname): @pass_context def export_json(context, doctype, path, name=None): "Export doclist as json to the given path, use '-' as name for Singles." - from frappe.core.page.data_import_tool import data_import_tool + from frappe.core.doctype.data_import import data_import for site in context.sites: try: frappe.init(site=site) frappe.connect() - data_import_tool.export_json(doctype, path, name=name) + data_import.export_json(doctype, path, name=name) finally: frappe.destroy() @@ -177,12 +179,12 @@ def export_json(context, doctype, path, name=None): @pass_context def export_csv(context, doctype, path): "Export data import template with data for DocType" - from frappe.core.page.data_import_tool import data_import_tool + from frappe.core.doctype.data_import import data_import for site in context.sites: try: frappe.init(site=site) frappe.connect() - data_import_tool.export_csv(doctype, path) + data_import.export_csv(doctype, path) finally: frappe.destroy() @@ -204,7 +206,7 @@ def export_fixtures(context): @pass_context def import_doc(context, path, force=False): "Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported" - from frappe.core.page.data_import_tool import data_import_tool + from frappe.core.doctype.data_import import data_import if not os.path.exists(path): path = os.path.join('..', path) @@ -216,7 +218,7 @@ def import_doc(context, path, force=False): try: frappe.init(site=site) frappe.connect() - data_import_tool.import_doc(path, overwrite=context.force) + data_import.import_doc(path, overwrite=context.force) finally: frappe.destroy() @@ -229,8 +231,8 @@ def import_doc(context, path, force=False): @pass_context def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True): - "Import CSV using data import tool" - from frappe.core.page.data_import_tool import importer + "Import CSV using data import" + from frappe.core.doctype.data_import import importer from frappe.utils.csvutils import read_csv_content site = get_site(context) @@ -300,6 +302,7 @@ def console(context): @click.command('run-tests') @click.option('--app', help="For App") @click.option('--doctype', help="For DocType") +@click.option('--doctype-list-path', help="Path to .txt file for list of doctypes. Example erpnext/tests/server/agriculture.txt") @click.option('--test', multiple=True, help="Specific test") @click.option('--driver', help="For Travis") @click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests") @@ -308,7 +311,7 @@ def console(context): @click.option('--junit-xml-output', help="Destination file path for junit xml report") @pass_context def run_tests(context, app=None, module=None, doctype=None, test=(), - driver=None, profile=False, junit_xml_output=False, ui_tests = False): + driver=None, profile=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None): "Run tests" import frappe.test_runner tests = test @@ -318,7 +321,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, force=context.force, profile=profile, junit_xml_output=junit_xml_output, - ui_tests = ui_tests) + ui_tests = ui_tests, doctype_list_path = doctype_list_path) if len(ret.failures) == 0 and len(ret.errors) == 0: ret = 0 @@ -327,10 +330,11 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), @click.command('run-ui-tests') @click.option('--app', help="App to run tests on, leave blank for all apps") -@click.option('--test', help="File name of the test you want to run") +@click.option('--test', help="Path to the specific test you want to run") +@click.option('--test-list', help="Path to the txt file with the list of test cases") @click.option('--profile', is_flag=True, default=False) @pass_context -def run_ui_tests(context, app=None, test=False, profile=False): +def run_ui_tests(context, app=None, test=False, test_list=False, profile=False): "Run UI tests" import frappe.test_runner @@ -338,7 +342,7 @@ def run_ui_tests(context, app=None, test=False, profile=False): frappe.init(site=site) frappe.connect() - ret = frappe.test_runner.run_ui_tests(app=app, test=test, verbose=context.verbose, + ret = frappe.test_runner.run_ui_tests(app=app, test=test, test_list=test_list, verbose=context.verbose, profile=profile) if len(ret.failures) == 0 and len(ret.errors) == 0: ret = 0 diff --git a/frappe/config/desktop.py b/frappe/config/desktop.py index 5ac41b59dd..76f02708b5 100644 --- a/frappe/config/desktop.py +++ b/frappe/config/desktop.py @@ -70,5 +70,5 @@ def get_data(): "icon": "octicon octicon-book", "color": '#FFAEDB', "hidden": 1, - }, + } ] diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 7c28372382..87f2b01614 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -82,7 +82,16 @@ def get_data(): "name": "Webhook", "description": _("Webhooks calling API requests into web apps"), } - + ] + }, + { + "label": _("Maps"), + "items": [ + { + "type": "doctype", + "name": "Google Maps", + "description": _("Google Maps integration"), + } ] } ] diff --git a/frappe/config/setup.py b/frappe/config/setup.py index 55ed2dbd9e..2dcc10335e 100644 --- a/frappe/config/setup.py +++ b/frappe/config/setup.py @@ -17,6 +17,11 @@ def get_data(): "type": "doctype", "name": "Role", "description": _("User Roles") + }, + { + "type": "doctype", + "name": "Role Profile", + "description": _("Role Profile") } ] }, @@ -81,6 +86,13 @@ def get_data(): "name": "Error Snapshot", "description": _("Log of error during requests.") }, + { + "type": "doctype", + "name": "Domain Settings", + "label": _("Domain Settings"), + "description": _("Enable / Disable Domains"), + "hide_count": True + }, ] }, { @@ -88,11 +100,11 @@ def get_data(): "icon": "fa fa-th", "items": [ { - "type": "page", - "name": "data-import-tool", + "type": "doctype", + "name": "Data Import", "label": _("Import / Export Data"), - "icon": "fa fa-upload", - "description": _("Import / Export Data from .csv files.") + "icon": "octicon octicon-cloud-upload", + "description": _("Import / Export Data from CSV and Excel files.") }, { "type": "doctype", diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index f746731754..9ccffab996 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -26,18 +26,17 @@ def load_address_and_contact(doc, key=None): doc.set_onload('addr_list', address_list) contact_list = [] - if doc.doctype != "Lead": - filters = [ - ["Dynamic Link", "link_doctype", "=", doc.doctype], - ["Dynamic Link", "link_name", "=", doc.name], - ["Dynamic Link", "parenttype", "=", "Contact"], - ] - contact_list = frappe.get_all("Contact", filters=filters, fields=["*"]) - - contact_list = sorted(contact_list, - lambda a, b: - (int(a.is_primary_contact - b.is_primary_contact)) or - (1 if a.modified - b.modified else 0), reverse=True) + filters = [ + ["Dynamic Link", "link_doctype", "=", doc.doctype], + ["Dynamic Link", "link_name", "=", doc.name], + ["Dynamic Link", "parenttype", "=", "Contact"], + ] + contact_list = frappe.get_all("Contact", filters=filters, fields=["*"]) + + contact_list = sorted(contact_list, + lambda a, b: + (int(a.is_primary_contact - b.is_primary_contact)) or + (1 if a.modified - b.modified else 0), reverse=True) doc.set_onload('contact_list', contact_list) @@ -147,4 +146,4 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil order_by="dt asc", as_list=True) all_doctypes = doctypes + _doctypes - return sorted(all_doctypes, key=lambda item: item[0]) \ No newline at end of file + return sorted(all_doctypes, key=lambda item: item[0]) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 23027449b1..9be54b4444 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -46,6 +46,11 @@ class Contact(Document): return None + def has_link(self, doctype, name): + for link in self.links: + if link.link_doctype==doctype and link.link_name== name: + return True + def has_common_link(self, doc): reference_links = [(link.link_doctype, link.link_name) for link in doc.links] for link in self.links: diff --git a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.js b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.js index 32261a9454..9e2fd0f68a 100644 --- a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.js +++ b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.js @@ -15,15 +15,14 @@ frappe.query_reports["Addresses And Contacts"] = { "name": ["in","Customer,Supplier,Sales Partner"], } } - }, - "default": "Customer" + } }, { "fieldname":"party_name", "label": __("Party Name"), "fieldtype": "Dynamic Link", "get_options": function() { - var party_type = frappe.query_report_filters_by_name.party_type.get_value(); + let party_type = frappe.query_report_filters_by_name.party_type.get_value(); if(!party_type) { frappe.throw(__("Please select Party Type first")); } diff --git a/frappe/core/doctype/activity_log/__init__.py b/frappe/core/doctype/activity_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/activity_log/activity_log.js b/frappe/core/doctype/activity_log/activity_log.js new file mode 100644 index 0000000000..97e49e4b34 --- /dev/null +++ b/frappe/core/doctype/activity_log/activity_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Activity Log', { + refresh: function() { + + } +}); diff --git a/frappe/core/doctype/activity_log/activity_log.json b/frappe/core/doctype/activity_log/activity_log.json new file mode 100644 index 0000000000..02360247af --- /dev/null +++ b/frappe/core/doctype/activity_log/activity_log.json @@ -0,0 +1,717 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "", + "beta": 0, + "creation": "2017-10-05 11:10:38.780133", + "custom": 0, + "description": "Keep track of all update feeds", + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subject", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Subject", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "content", + "fieldtype": "Text Editor", + "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": "Message", + "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, + "width": "400" + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "additional_info", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "More Information", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Now", + "fieldname": "communication_date", + "fieldtype": "Datetime", + "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": "Date", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_7", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "operation", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Operation", + "length": 0, + "no_copy": 0, + "options": "\nLogin\nLogout", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "\nSuccess\nFailed\nLinked\nClosed", + "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, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "reference_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "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": "Reference DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "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": "Reference Name", + "length": 0, + "no_copy": 0, + "options": "reference_doctype", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_owner", + "fieldtype": "Read Only", + "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": "Reference Owner", + "length": 0, + "no_copy": 0, + "options": "reference_name.owner", + "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": 1, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_14", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "timeline_doctype", + "fieldtype": "Link", + "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": "Timeline DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "timeline_name", + "fieldtype": "Dynamic Link", + "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": "Timeline Name", + "length": 0, + "no_copy": 0, + "options": "timeline_doctype", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "link_doctype", + "fieldtype": "Link", + "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": "Link DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "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": "Link Name", + "length": 0, + "no_copy": 0, + "options": "link_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "__user", + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 1, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "full_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Full Name", + "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 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-comment", + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-11-21 12:39:23.659308", + "modified_by": "Administrator", + "module": "Core", + "name": "Activity Log", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 0 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 0 + }, + { + "amend": 0, + "apply_user_permissions": 1, + "cancel": 0, + "create": 0, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 1, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "All", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "user_permission_doctypes": "[\"Email Account\"]", + "write": 0 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "search_fields": "subject", + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "subject", + "track_changes": 1, + "track_seen": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/activity_log/activity_log.py b/frappe/core/doctype/activity_log/activity_log.py new file mode 100644 index 0000000000..33e444650b --- /dev/null +++ b/frappe/core/doctype/activity_log/activity_log.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe import _ +from frappe.utils import get_fullname, now +from frappe.model.document import Document +from frappe.core.utils import get_parent_doc, set_timeline_doc +import frappe + +class ActivityLog(Document): + def before_insert(self): + self.full_name = get_fullname(self.user) + self.date = now() + + def validate(self): + self.set_status() + set_timeline_doc(self) + + def set_status(self): + if not self.is_new(): + return + + if self.reference_doctype and self.reference_name: + self.status = "Linked" + + def on_trash(self): # pylint: disable=no-self-use + frappe.throw(_("Sorry! You cannot delete auto-generated comments")) + +def on_doctype_update(): + """Add indexes in `tabActivity Log`""" + frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"]) + frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"]) + frappe.db.add_index("Activity Log", ["link_doctype", "link_name"]) + +def add_authentication_log(subject, user, operation="Login", status="Success"): + frappe.get_doc({ + "doctype": "Activity Log", + "user": user, + "status": status, + "subject": subject, + "operation": operation, + }).insert(ignore_permissions=True) + +def clear_authentication_logs(): + """clear 100 day old authentication logs""" + frappe.db.sql("""delete from `tabActivity Log` where \ + creation" @@ -393,7 +446,7 @@ def filter_email_list(doc, email_list, exclude, is_cc=False): return filtered def get_owner_email(doc): - owner = doc.get_parent_doc().owner + owner = get_parent_doc(doc).owner return get_formatted_email(owner) or owner def get_assignees(doc): @@ -412,11 +465,11 @@ def get_attach_link(doc, print_format): "doctype": doc.reference_doctype, "name": doc.reference_name, "print_format": print_format, - "key": doc.get_parent_doc().get_signature() + "key": get_parent_doc(doc).get_signature() }) def sendmail(communication_name, print_html=None, print_format=None, attachments=None, - recipients=None, cc=None, lang=None, session=None): + recipients=None, cc=None, bcc=None, lang=None, session=None): try: if lang: @@ -432,11 +485,11 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments try: communication = frappe.get_doc("Communication", communication_name) communication._notify(print_html=print_html, print_format=print_format, attachments=attachments, - recipients=recipients, cc=cc) + recipients=recipients, cc=cc, bcc=bcc) - except MySQLdb.OperationalError as e: + except pymysql.InternalError as e: # deadlock, try again - if e.args[0]==1213: + if e.args[0] == ER.LOCK_DEADLOCK: frappe.db.rollback() time.sleep(1) continue @@ -453,6 +506,7 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments "attachments": attachments, "recipients": recipients, "cc": cc, + "bcc": bcc, "lang": lang })) frappe.logger(__name__).error(traceback) diff --git a/frappe/core/doctype/communication/test_communication.js b/frappe/core/doctype/communication/test_communication.js new file mode 100644 index 0000000000..2fd95b34b0 --- /dev/null +++ b/frappe/core/doctype/communication/test_communication.js @@ -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: Communication", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Communication + () => frappe.tests.make('Communication', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/core/page/data_import_tool/README.md b/frappe/core/doctype/data_import/README.md similarity index 100% rename from frappe/core/page/data_import_tool/README.md rename to frappe/core/doctype/data_import/README.md diff --git a/frappe/core/doctype/data_import/__init__.py b/frappe/core/doctype/data_import/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js new file mode 100644 index 0000000000..17408e33b7 --- /dev/null +++ b/frappe/core/doctype/data_import/data_import.js @@ -0,0 +1,261 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Data Import', { + onload: function(frm) { + frm.set_query("reference_doctype", function() { + return { + "filters": { + "issingle": 0, + "istable": 0, + "name": ['in', frappe.boot.user.can_import] + } + }; + }); + + frappe.realtime.on("data_import_progress", function(data) { + if (data.data_import === frm.doc.name) { + if (data.reload && data.reload === true) { + frm.reload_doc(); + } + if (data.progress) { + let progress_bar = $(frm.dashboard.progress_area).find(".progress-bar"); + if (progress_bar) { + $(progress_bar).removeClass("progress-bar-danger").addClass("progress-bar-success progress-bar-striped"); + $(progress_bar).css("width", data.progress+"%"); + } + } + } + }); + }, + + refresh: function(frm) { + frm.disable_save(); + frm.dashboard.clear_headline(); + if (frm.doc.reference_doctype && !frm.doc.import_file) { + frm.dashboard.add_comment(__('Please attach a file to import')); + } else { + if (frm.doc.import_status) { + frm.dashboard.add_comment(frm.doc.import_status); + + if (frm.doc.import_status==="In Progress") { + frm.dashboard.add_progress("Data Import Progress", "0"); + frm.set_read_only(); + frm.refresh_fields(); + } + } + } + + if (frm.doc.reference_doctype) { + frappe.model.with_doctype(frm.doc.reference_doctype); + } + + frm.add_custom_button(__("Help"), function() { + frappe.help.show_video("6wiriRKPhmg"); + }); + + if(frm.doc.reference_doctype && frm.doc.docstatus === 0) { + frm.add_custom_button(__("Download template"), function() { + frappe.data_import.download_dialog(frm).show(); + }); + } + + if (frm.doc.reference_doctype && frm.doc.import_file && frm.doc.total_rows && frm.doc.docstatus === 0) { + frm.page.set_primary_action(__("Start Import"), function() { + frappe.call({ + method: "frappe.core.doctype.data_import.data_import.import_data", + args: { + data_import: frm.doc.name + } + }); + }).addClass('btn btn-primary'); + } + + if (frm.doc.log_details) { + frm.events.create_log_table(frm); + } else { + $(frm.fields_dict.import_log.wrapper).empty(); + } + }, + + reference_doctype: function(frm) { + if (frm.doc.reference_doctype) { + frm.save(); + } + }, + + // import_file: function(frm) { + // frm.save(); + // }, + + overwrite: function(frm) { + if (frm.doc.overwrite === 0) { + frm.doc.only_update = 0; + } + frm.save(); + }, + + only_update: function(frm) { + frm.save(); + }, + + submit_after_import: function(frm) { + frm.save(); + }, + + skip_errors: function(frm) { + frm.save(); + }, + + ignore_encoding_errors: function(frm) { + frm.save(); + }, + + no_email: function(frm) { + frm.save(); + }, + + show_only_errors: function(frm) { + frm.events.create_log_table(frm); + }, + + create_log_table: function(frm) { + let msg = JSON.parse(frm.doc.log_details); + var $log_wrapper = $(frm.fields_dict.import_log.wrapper).empty(); + $(frappe.render_template("log_details", {data: msg.messages, show_only_errors: frm.doc.show_only_errors, + import_status: frm.doc.import_status})).appendTo($log_wrapper); + } +}); + +frappe.provide('frappe.data_import'); +frappe.data_import.download_dialog = function(frm) { + var dialog; + const filter_fields = df => frappe.model.is_value_type(df) && !df.hidden; + const get_fields = dt => frappe.meta.get_docfields(dt).filter(filter_fields); + + const get_doctypes = parentdt => { + return [parentdt].concat( + frappe.meta.get_table_fields(parentdt).map(df => df.options) + ); + }; + + const get_doctype_checkbox_fields = () => { + return dialog.fields.filter(df => df.fieldname.endsWith('_fields')) + .map(df => dialog.fields_dict[df.fieldname]); + } + + const doctype_fields = get_fields(frm.doc.reference_doctype) + .map(df => ({ + label: df.label, + value: df.fieldname, + checked: 1 + })); + + let fields = [ + { + "label": __("Select Columns"), + "fieldname": "select_columns", + "fieldtype": "Select", + "options": "All\nMandatory\nManually", + "reqd": 1, + "onchange": function() { + const fields = get_doctype_checkbox_fields(); + fields.map(f => f.toggle(this.value === 'Manually')); + } + }, + { + "label": __("File Type"), + "fieldname": "file_type", + "fieldtype": "Select", + "options": "Excel\nCSV", + "default": "Excel" + }, + { + "label": __("Download with Data"), + "fieldname": "with_data", + "fieldtype": "Check" + }, + { + "label": frm.doc.reference_doctype, + "fieldname": "doctype_fields", + "fieldtype": "MultiCheck", + "options": doctype_fields, + "columns": 2, + "hidden": 1 + } + ]; + + const child_table_fields = frappe.meta.get_table_fields(frm.doc.reference_doctype) + .map(df => { + return { + "label": df.options, + "fieldname": df.fieldname + '_fields', + "fieldtype": "MultiCheck", + "options": frappe.meta.get_docfields(df.options) + .filter(filter_fields) + .map(df => ({ + label: df.label, + value: df.fieldname, + checked: 1 + })), + "columns": 2, + "hidden": 1 + }; + }); + + fields = fields.concat(child_table_fields); + + dialog = new frappe.ui.Dialog({ + title: __('Download Template'), + fields: fields, + primary_action: function(values) { + var data = values; + if (frm.doc.reference_doctype) { + var export_params = () => { + let columns = {}; + if (values.select_columns === 'All') { + columns = get_doctypes(frm.doc.reference_doctype).reduce((columns, doctype) => { + columns[doctype] = get_fields(doctype).map(df => df.fieldname); + return columns; + }, {}); + } else if (values.select_columns === 'Mandatory') { + // only reqd child tables + const doctypes = [frm.doc.reference_doctype].concat( + frappe.meta.get_table_fields(frm.doc.reference_doctype) + .filter(df => df.reqd).map(df => df.options) + ); + + columns = doctypes.reduce((columns, doctype) => { + columns[doctype] = get_fields(doctype).filter(df => df.reqd).map(df => df.fieldname); + return columns; + }, {}); + } else if (values.select_columns === 'Manually') { + columns = get_doctype_checkbox_fields().reduce((columns, field) => { + const options = field.get_checked_options(); + columns[field.df.label] = options; + return columns; + }, {}); + } + + return { + doctype: frm.doc.reference_doctype, + parent_doctype: frm.doc.reference_doctype, + select_columns: JSON.stringify(columns), + with_data: data.with_data ? 'Yes' : 'No', + all_doctypes: 'Yes', + from_data_import: 'Yes', + excel_format: data.file_type === 'Excel' ? 'Yes' : 'No' + }; + }; + let get_template_url = '/api/method/frappe.core.doctype.data_import.exporter.get_template'; + open_url_post(get_template_url, export_params()); + } else { + frappe.msgprint(__("Please select the Document Type.")); + } + dialog.hide(); + }, + primary_action_label: __('Download') + }); + + return dialog; +}; diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json new file mode 100644 index 0000000000..e598a32922 --- /dev/null +++ b/frappe/core/doctype/data_import/data_import.json @@ -0,0 +1,661 @@ +{ + "allow_copy": 1, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "", + "beta": 0, + "creation": "2016-12-09 14:27:32.720061", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 1, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "", + "columns": 0, + "depends_on": "eval:(!doc.__islocal)", + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "fieldname": "import_file", + "fieldtype": "Attach", + "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": "Attach file for Import", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval: doc.import_status == \"Partially Successful\"", + "description": "This is the template file generated with only the rows having some error. You should use this file for correction and import.", + "fieldname": "error_file", + "fieldtype": "Attach", + "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": "Generated File", + "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, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "", + "columns": 0, + "depends_on": "eval:(!doc.__islocal)", + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "depends_on": "", + "description": "If you are updating/overwriting already created records.", + "fieldname": "overwrite", + "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": "Update records", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "depends_on": "overwrite", + "description": "If you don't want to create any new records while updating the older records.", + "fieldname": "only_update", + "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": "Don't create new records", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "If this is checked, rows with valid data will be imported and invalid rows will be dumped into a new file for you to import later.\n", + "fieldname": "skip_errors", + "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": "Skip rows with errors", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "depends_on": "", + "fieldname": "submit_after_import", + "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": "Submit after importing", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "depends_on": "", + "fieldname": "ignore_encoding_errors", + "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": "Ignore encoding errors", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "depends_on": "", + "fieldname": "no_email", + "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": "Do not send 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, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "eval: doc.import_status == \"Failed\"", + "columns": 0, + "depends_on": "import_status", + "fieldname": "import_detail", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Import Log", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "fieldname": "import_status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Import Status", + "length": 0, + "no_copy": 0, + "options": "\nSuccessful\nFailed\nIn Progress\nPartially Successful", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "show_only_errors", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Show only errors", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "", + "depends_on": "import_status", + "fieldname": "import_log", + "fieldtype": "HTML", + "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": "Import Log", + "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": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "fieldname": "log_details", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Log Details", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "amended_from", + "fieldtype": "Link", + "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": "Amended From", + "length": 0, + "no_copy": 1, + "options": "Data Import", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "total_rows", + "fieldtype": "Int", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Rows", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 1, + "issingle": 0, + "istable": 0, + "max_attachments": 1, + "modified": "2017-12-15 14:49:24.622128", + "modified_by": "Administrator", + "module": "Core", + "name": "Data Import", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 1, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "", + "track_changes": 1, + "track_seen": 1 +} \ No newline at end of file diff --git a/frappe/core/page/data_import_tool/data_import_tool.py b/frappe/core/doctype/data_import/data_import.py similarity index 63% rename from frappe/core/page/data_import_tool/data_import_tool.py rename to frappe/core/doctype/data_import/data_import.py index 8338848561..95764709a1 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -1,44 +1,67 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals, print_function +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt +from __future__ import unicode_literals import frappe, os from frappe import _ import frappe.modules.import_file +from frappe.model.document import Document +from frappe.utils.data import format_datetime +from frappe.core.doctype.data_import.importer import upload +from frappe.utils.background_jobs import enqueue -@frappe.whitelist() -def get_data_keys(): - return frappe._dict({ - "data_separator": _('Start entering data below this line'), - "main_table": _("Table") + ":", - "parent_table": _("Parent Table") + ":", - "columns": _("Column Name") + ":", - "doctype": _("DocType") + ":" - }) -@frappe.whitelist() -def get_doctypes(): - return frappe.get_user()._get("can_import") +class DataImport(Document): + def autoname(self): + self.name = "Import on "+ format_datetime(self.creation) + + def validate(self): + if not self.import_file: + self.db_set("total_rows", 0) + if self.import_status == "In Progress": + frappe.throw(_("Can't save the form as data import is in progress.")) + + # validate the template just after the upload + # if there is total_rows in the doc, it means that the template is already validated and error free + if self.import_file and not self.total_rows: + upload(data_import_doc=self, from_data_import="Yes", validate_template=True) + @frappe.whitelist() -def get_doctype_options(): - doctype = frappe.form_dict['doctype'] - return [doctype] + [d.options for d in frappe.get_meta(doctype).get_table_fields()] +def import_data(data_import): + frappe.db.set_value("Data Import", data_import, "import_status", "In Progress", update_modified=False) + frappe.publish_realtime("data_import_progress", {"progress": "0", + "data_import": data_import, "reload": True}, user=frappe.session.user) + enqueue(upload, queue='default', timeout=6000, event='data_import', + data_import_doc=data_import, from_data_import="Yes", validate_template=False) + + +def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False, + insert=False, submit=False, pre_process=None): + if os.path.isdir(path): + files = [os.path.join(path, f) for f in os.listdir(path)] + else: + files = [path] + + for f in files: + if f.endswith(".json"): + frappe.flags.mute_emails = True + frappe.modules.import_file.import_file_by_path(f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True) + frappe.flags.mute_emails = False + frappe.db.commit() + elif f.endswith(".csv"): + import_file_by_path(f, ignore_links=ignore_links, overwrite=overwrite, submit=submit, pre_process=pre_process) + frappe.db.commit() + def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False, pre_process=None, no_email=True): from frappe.utils.csvutils import read_csv_content - from frappe.core.page.data_import_tool.importer import upload print("Importing " + path) with open(path, "r") as infile: upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, no_email=no_email, overwrite=overwrite, - submit_after_import=submit, pre_process=pre_process) + submit_after_import=submit, pre_process=pre_process) -def export_csv(doctype, path): - from frappe.core.page.data_import_tool.exporter import get_template - with open(path, "wb") as csvfile: - get_template(doctype=doctype, all_doctypes="Yes", with_data="Yes") - csvfile.write(frappe.response.result.encode("utf-8")) def export_json(doctype, path, filters=None, or_filters=None, name=None): def post_process(out): @@ -71,6 +94,14 @@ def export_json(doctype, path, filters=None, or_filters=None, name=None): with open(path, "w") as outfile: outfile.write(frappe.as_json(out)) + +def export_csv(doctype, path): + from frappe.core.doctype.data_import.exporter import get_template + with open(path, "wb") as csvfile: + get_template(doctype=doctype, all_doctypes="Yes", with_data="Yes") + csvfile.write(frappe.response.result.encode("utf-8")) + + @frappe.whitelist() def export_fixture(doctype, app): if frappe.session.user != "Administrator": @@ -80,21 +111,3 @@ def export_fixture(doctype, app): os.mkdir(frappe.get_app_path(app, "fixtures")) export_json(doctype, frappe.get_app_path(app, "fixtures", frappe.scrub(doctype) + ".json")) - - -def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False, - insert=False, submit=False, pre_process=None): - if os.path.isdir(path): - files = [os.path.join(path, f) for f in os.listdir(path)] - else: - files = [path] - - for f in files: - if f.endswith(".json"): - frappe.flags.mute_emails = True - frappe.modules.import_file.import_file_by_path(f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True) - frappe.flags.mute_emails = False - frappe.db.commit() - elif f.endswith(".csv"): - import_file_by_path(f, ignore_links=ignore_links, overwrite=overwrite, submit=submit, pre_process=pre_process) - frappe.db.commit() diff --git a/frappe/core/doctype/data_import/data_import_list.js b/frappe/core/doctype/data_import/data_import_list.js new file mode 100644 index 0000000000..2a4dc5383c --- /dev/null +++ b/frappe/core/doctype/data_import/data_import_list.js @@ -0,0 +1,24 @@ +frappe.listview_settings['Data Import'] = { + add_fields: ["import_status"], + has_indicator_for_draft: 1, + get_indicator: function(doc) { + + let status = { + 'Successful': [__("Success"), "green", "import_status,=,Successful"], + 'Partially Successful': [__("Partial Success"), "blue", "import_status,=,Partially Successful"], + 'In Progress': [__("In Progress"), "orange", "import_status,=,In Progress"], + 'Failed': [__("Failed"), "red", "import_status,=,Failed"], + 'Pending': [__("Pending"), "orange", "import_status,=,"] + } + + if (doc.import_status) { + return status[doc.import_status]; + } + + if (doc.docstatus == 0) { + return status['Pending']; + } + + return status['Pending']; + } +}; diff --git a/frappe/core/page/data_import_tool/exporter.py b/frappe/core/doctype/data_import/exporter.py similarity index 98% rename from frappe/core/page/data_import_tool/exporter.py rename to frappe/core/doctype/data_import/exporter.py index 7920059957..9a8ca42b24 100644 --- a/frappe/core/page/data_import_tool/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -9,7 +9,7 @@ import frappe.permissions import re, csv, os from frappe.utils.csvutils import UnicodeWriter from frappe.utils import cstr, formatdate, format_datetime -from frappe.core.page.data_import_tool.data_import_tool import get_data_keys +from frappe.core.doctype.data_import.importer import get_data_keys from six import string_types reflags = { @@ -165,6 +165,8 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data return 'Integer' elif docfield.fieldtype == "Check": return "0 or 1" + elif docfield.fieldtype in ["Date", "Datetime"]: + return cstr(frappe.defaults.get_defaults().date_format) elif hasattr(docfield, "info"): return docfield.info else: diff --git a/frappe/core/page/data_import_tool/importer.py b/frappe/core/doctype/data_import/importer.py similarity index 58% rename from frappe/core/page/data_import_tool/importer.py rename to frappe/core/doctype/data_import/importer.py index a49110691b..95336c84b3 100644 --- a/frappe/core/page/data_import_tool/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -15,35 +15,54 @@ from frappe.utils.csvutils import getlink from frappe.utils.dateutils import parse_date from frappe.utils.file_manager import save_url -from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url -from frappe.core.page.data_import_tool.data_import_tool import get_data_keys +from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_url_to_form from six import text_type, string_types + @frappe.whitelist() -def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, no_email=True, overwrite=None, - update_only = None, ignore_links=False, pre_process=None, via_console=False, from_data_import="No", - skip_errors = True): - """upload data""" +def get_data_keys(): + return frappe._dict({ + "data_separator": _('Start entering data below this line'), + "main_table": _("Table") + ":", + "parent_table": _("Parent Table") + ":", + "columns": _("Column Name") + ":", + "doctype": _("DocType") + ":" + }) - frappe.flags.in_import = True - # extra input params - params = json.loads(frappe.form_dict.get("params") or '{}') +@frappe.whitelist() +def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, no_email=True, overwrite=None, + update_only = None, ignore_links=False, pre_process=None, via_console=False, from_data_import="No", + skip_errors = True, data_import_doc=None, validate_template=False): + """upload data""" - if params.get("submit_after_import"): - submit_after_import = True - if params.get("ignore_encoding_errors"): - ignore_encoding_errors = True - if not params.get("no_email"): - no_email = False - if params.get('update_only'): - update_only = True - if params.get('from_data_import'): - from_data_import = params.get('from_data_import') - if not params.get('skip_errors'): - skip_errors = params.get('skip_errors') + if data_import_doc and isinstance(data_import_doc, string_types): + data_import_doc = frappe.get_doc("Data Import", data_import_doc) + if data_import_doc and from_data_import == "Yes": + no_email = data_import_doc.no_email + ignore_encoding_errors = data_import_doc.ignore_encoding_errors + update_only = data_import_doc.only_update + submit_after_import = data_import_doc.submit_after_import + overwrite = data_import_doc.overwrite + skip_errors = data_import_doc.skip_errors + else: + # extra input params + params = json.loads(frappe.form_dict.get("params") or '{}') + if params.get("submit_after_import"): + submit_after_import = True + if params.get("ignore_encoding_errors"): + ignore_encoding_errors = True + if not params.get("no_email"): + no_email = False + if params.get('update_only'): + update_only = True + if params.get('from_data_import'): + from_data_import = params.get('from_data_import') + if not params.get('skip_errors'): + skip_errors = params.get('skip_errors') + frappe.flags.in_import = True frappe.flags.mute_emails = no_email def get_data_keys_definition(): @@ -55,9 +74,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, def check_data_length(): max_rows = 5000 if not data: - frappe.throw(_("No data found")) - elif not via_console and len(data) > max_rows: - frappe.throw(_("Only allowed {0} rows in one import").format(max_rows)) + frappe.throw(_("No data found in the file. Please reattach the new file with data.")) def get_start_row(): for i, row in enumerate(rows): @@ -110,7 +127,10 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, def get_doc(start_idx): if doctypes: doc = {} + attachments = [] + last_error_row_idx = None for idx in range(start_idx, len(rows)): + last_error_row_idx = idx # pylint: disable=W0612 if (not doc) or main_doc_empty(rows[idx]): for dt, parentfield in doctypes: d = {} @@ -119,6 +139,9 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, fieldname = column_idx_to_fieldname[(dt, parentfield)][column_idx] fieldtype = column_idx_to_fieldtype[(dt, parentfield)][column_idx] + if not fieldname or not rows[idx][column_idx]: + continue + d[fieldname] = rows[idx][column_idx] if fieldtype in ("Int", "Check"): d[fieldname] = cint(d[fieldname]) @@ -158,7 +181,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, if dt == doctype: doc.update(d) else: - if not overwrite: + if not overwrite and doc.get("name"): d['parent'] = doc["name"] d['parenttype'] = doctype d['parentfield'] = parentfield @@ -166,7 +189,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, else: break - return doc + return doc, attachments, last_error_row_idx else: doc = frappe._dict(zip(columns, rows[start_idx][1:])) doc['doctype'] = doctype @@ -175,6 +198,22 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, def main_doc_empty(row): return not (row and ((len(row) > 1 and row[1]) or (len(row) > 2 and row[2]))) + def validate_naming(doc): + autoname = frappe.get_meta(doctype).autoname + + if ".#" in autoname or "hash" in autoname: + autoname = "" + elif autoname[0:5] == 'field': + autoname = autoname[6:] + elif autoname=='naming_series:': + autoname = 'naming_series' + else: + return True + + if (autoname and autoname not in doc) or (autoname and not doc[autoname]): + frappe.throw(_("{0} is a mandatory field".format(autoname))) + return True + users = frappe.db.sql_list("select name from tabUser") def prepare_for_insert(doc): # don't block data import if user is not set @@ -219,19 +258,18 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, save_url(file_url, None, doctype, docname, "Home/Attachments", 0) # header + filename, file_extension = ['',''] if not rows: - from frappe.utils.file_manager import get_file_doc - file_doc = get_file_doc(dt='', dn="Data Import", folder='Home', is_private=1) - filename, file_extension = os.path.splitext(file_doc.file_name) + from frappe.utils.file_manager import get_file # get_file_doc + fname, fcontent = get_file(data_import_doc.import_file) + filename, file_extension = os.path.splitext(fname) if file_extension == '.xlsx' and from_data_import == 'Yes': from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file - rows = read_xlsx_file_from_attached_file(file_id=file_doc.name) + rows = read_xlsx_file_from_attached_file(file_id=data_import_doc.import_file) elif file_extension == '.csv': - from frappe.utils.file_manager import get_file from frappe.utils.csvutils import read_csv_content - fname, fcontent = get_file(file_doc.name) rows = read_csv_content(fcontent, ignore_encoding_errors) else: @@ -245,7 +283,9 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, doctypes = [] column_idx_to_fieldname = {} column_idx_to_fieldtype = {} - attachments = [] + + if skip_errors: + data_rows_with_error = header if submit_after_import and not cint(frappe.db.get_value("DocType", doctype, "is_submittable")): @@ -261,14 +301,19 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, frappe.flags.mute_emails = False return {"messages": [_("Not allowed to Import") + ": " + _(doctype)], "error": True} - # allow limit rows to be uploaded + # Throw expception in case of the empty data file check_data_length() make_column_map() + total = len(data) + + if validate_template: + if total: + data_import_doc.total_rows = total + return True if overwrite==None: overwrite = params.get('overwrite') - # delete child rows (if parenttype) parentfield = None if parenttype: @@ -277,13 +322,12 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, if overwrite: delete_child_rows(data, doctype) - ret = [] - - def log(msg): + import_log = [] + def log(**kwargs): if via_console: - print(msg.encode('utf-8')) + print((kwargs.get("title") + kwargs.get("message")).encode('utf-8')) else: - ret.append(msg) + import_log.append(kwargs) def as_link(doctype, name): if via_console: @@ -291,8 +335,14 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, else: return getlink(doctype, name) - error = False - total = len(data) + # publish realtime task update + def publish_progress(achieved, reload=False): + if data_import_doc: + frappe.publish_realtime("data_import_progress", {"progress": str(int(100.0*achieved/total)), + "data_import": data_import_doc.name, "reload": reload}, user=frappe.session.user) + + + error_flag = rollback_flag = False for i, row in enumerate(data): # bypass empty rows if main_doc_empty(row): @@ -301,23 +351,21 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, row_idx = i + start_row doc = None - # publish task_update - frappe.publish_realtime("data_import_progress", {"progress": [i, total]}, - user=frappe.session.user) + publish_progress(i) try: - doc = get_doc(row_idx) + doc, attachments, last_error_row_idx = get_doc(row_idx) + validate_naming(doc) if pre_process: pre_process(doc) + original = None if parentfield: parent = frappe.get_doc(parenttype, doc["parent"]) doc = parent.append(parentfield, doc) parent.save() - log('Inserted row for %s at #%s' % (as_link(parenttype, - doc.parent),text_type(doc.idx))) else: - if overwrite and doc["name"] and frappe.db.exists(doctype, doc["name"]): + if overwrite and doc.get("name") and frappe.db.exists(doctype, doc["name"]): original = frappe.get_doc(doctype, doc["name"]) original_name = original.name original.update(doc) @@ -325,7 +373,6 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, original.name = original_name original.flags.ignore_links = ignore_links original.save() - log('Updated row (#%d) %s' % (row_idx + 1, as_link(original.doctype, original.name))) doc = original else: if not update_only: @@ -333,29 +380,53 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, prepare_for_insert(doc) doc.flags.ignore_links = ignore_links doc.insert() - log('Inserted row (#%d) %s' % (row_idx + 1, as_link(doc.doctype, doc.name))) - else: - log('Ignored row (#%d) %s' % (row_idx + 1, row[1])) if attachments: # check file url and create a File document for file_url in attachments: attach_file_to_doc(doc.doctype, doc.name, file_url) if submit_after_import: doc.submit() - log('Submitted row (#%d) %s' % (row_idx + 1, as_link(doc.doctype, doc.name))) + + # log errors + if parentfield: + log(**{"row": doc.idx, "title": 'Inserted row for "%s"' % (as_link(parenttype, doc.parent)), + "link": get_url_to_form(parenttype, doc.parent), "message": 'Document successfully saved', "indicator": "green"}) + elif submit_after_import: + log(**{"row": row_idx + 1, "title":'Submitted row for "%s"' % (as_link(doc.doctype, doc.name)), + "message": "Document successfully submitted", "link": get_url_to_form(doc.doctype, doc.name), "indicator": "blue"}) + elif original: + log(**{"row": row_idx + 1,"title":'Updated row for "%s"' % (as_link(doc.doctype, doc.name)), + "message": "Document successfully updated", "link": get_url_to_form(doc.doctype, doc.name), "indicator": "green"}) + elif not update_only: + log(**{"row": row_idx + 1, "title":'Inserted row for "%s"' % (as_link(doc.doctype, doc.name)), + "message": "Document successfully saved", "link": get_url_to_form(doc.doctype, doc.name), "indicator": "green"}) + else: + log(**{"row": row_idx + 1, "title":'Ignored row for %s' % (row[1]), "link": None, + "message": "Document updation ignored", "indicator": "orange"}) + except Exception as e: - if not skip_errors: - error = True - if doc: - frappe.errprint(doc if isinstance(doc, dict) else doc.as_dict()) - err_msg = frappe.local.message_log and "\n\n".join(frappe.local.message_log) or cstr(e) - log('Error for row (#%d) %s : %s' % (row_idx + 1, - len(row)>1 and row[1] or "", err_msg)) - frappe.errprint(frappe.get_traceback()) + error_flag = True + err_msg = frappe.local.message_log and "\n".join([json.loads(msg).get('message') for msg in frappe.local.message_log]) or cstr(e) + error_trace = frappe.get_traceback() + if error_trace: + error_log_doc = frappe.log_error(error_trace) + error_link = get_url_to_form("Error Log", error_log_doc.name) + else: + error_link = None + log(**{"row": row_idx + 1, "title":'Error for row %s' % (len(row)>1 and row[1] or ""), "message": err_msg, + "indicator": "red", "link":error_link}) + # data with error to create a new file + # include the errored data in the last row as last_error_row_idx will not be updated for the last row + if skip_errors: + if last_error_row_idx == len(rows)-1: + last_error_row_idx = len(rows) + data_rows_with_error += rows[row_idx:last_error_row_idx] + else: + rollback_flag = True finally: frappe.local.message_log = [] - if error: + if rollback_flag: frappe.db.rollback() else: frappe.db.commit() @@ -363,7 +434,42 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, frappe.flags.mute_emails = False frappe.flags.in_import = False - return {"messages": ret, "error": error} + log_message = {"messages": import_log, "error": error_flag} + if data_import_doc: + data_import_doc.log_details = json.dumps(log_message) + + import_status = None + if error_flag and data_import_doc.skip_errors and len(data) != len(data_rows_with_error): + import_status = "Partially Successful" + # write the file with the faulty row + from frappe.utils.file_manager import save_file + file_name = 'error_' + filename + file_extension + if file_extension == '.xlsx': + from frappe.utils.xlsxutils import make_xlsx + xlsx_file = make_xlsx(data_rows_with_error, "Data Import Template") + file_data = xlsx_file.getvalue() + else: + from frappe.utils.csvutils import to_csv + file_data = to_csv(data_rows_with_error) + error_data_file = save_file(file_name, file_data, "Data Import", + data_import_doc.name, "Home/Attachments") + data_import_doc.error_file = error_data_file.file_url + + elif error_flag: + import_status = "Failed" + else: + import_status = "Successful" + + data_import_doc.import_status = import_status + data_import_doc.save() + if data_import_doc.import_status in ["Successful", "Partially Successful"]: + data_import_doc.submit() + publish_progress(100, True) + else: + publish_progress(0, True) + frappe.db.commit() + else: + return log_message def get_parent_field(doctype, parenttype): parentfield = None diff --git a/frappe/core/doctype/data_import/log_details.html b/frappe/core/doctype/data_import/log_details.html new file mode 100644 index 0000000000..ae6c02ac04 --- /dev/null +++ b/frappe/core/doctype/data_import/log_details.html @@ -0,0 +1,38 @@ +
+
+ + + + + + + + {% for row in data %} + {% if (!show_only_errors) || (show_only_errors && row.indicator == "red") %} + + + + + + {% endif %} + {% endfor %} +
{{ __("Row No") }} {{ __("Row Status") }} {{ __("Message") }}
+ {{ row.row }} + + {{ row.title }} + + {% if (import_status != "Failed" || (row.indicator == "red")) { %} + {{ row.message }} + {% if row.link %} + + + + + + {% endif %} + {% } else { %} + {{ __("Document can't saved.") }} + {% } %} +
+
+
\ No newline at end of file diff --git a/frappe/core/doctype/data_import/test_data_import.py b/frappe/core/doctype/data_import/test_data_import.py new file mode 100644 index 0000000000..30dc9d173c --- /dev/null +++ b/frappe/core/doctype/data_import/test_data_import.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestDataImport(unittest.TestCase): + pass diff --git a/frappe/core/doctype/deleted_document/deleted_document.py b/frappe/core/doctype/deleted_document/deleted_document.py index e01e9b08bf..7dbd4d2645 100644 --- a/frappe/core/doctype/deleted_document/deleted_document.py +++ b/frappe/core/doctype/deleted_document/deleted_document.py @@ -17,7 +17,9 @@ def restore(name): try: doc.insert() except frappe.DocstatusTransitionError: - frappe.throw(_("Cannot restore Cancelled Document")) + frappe.msgprint(_("Cancelled Document restored as Draft")) + doc.docstatus = 0 + doc.insert() doc.add_comment('Edit', _('restored {0} as {1}').format(deleted.deleted_name, doc.name)) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index b00b0c7b07..b1e3e6f0ca 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -1,1380 +1,1380 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-02-22 01:27:33", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "hash", + "beta": 0, + "creation": "2013-02-22 01:27:33", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "label_and_type", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "label_and_type", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "label", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Label", - "length": 0, - "no_copy": 0, - "oldfieldname": "label", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "163", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "unique": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "fieldname": "label", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Label", + "length": 0, + "no_copy": 0, + "oldfieldname": "label", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "163", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "unique": 0, "width": "163" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "default": "Data", - "fieldname": "fieldtype", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "fieldtype", - "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Type", + "length": 0, + "no_copy": 0, + "oldfieldname": "fieldtype", + "oldfieldtype": "Select", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "fieldname", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "fieldname", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "fieldname": "fieldname", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Name", + "length": 0, + "no_copy": 0, + "oldfieldname": "fieldname", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reqd", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Mandatory", - "length": 0, - "no_copy": 0, - "oldfieldname": "reqd", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reqd", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Mandatory", + "length": 0, + "no_copy": 0, + "oldfieldname": "reqd", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", - "description": "Set non-standard precision for a Float or Currency field", - "fieldname": "precision", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Precision", - "length": 0, - "no_copy": 0, - "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", + "description": "Set non-standard precision for a Float or Currency field", + "fieldname": "precision", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Precision", + "length": 0, + "no_copy": 0, + "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9", + "permlevel": 0, + "print_hide": 1, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)", - "fieldname": "length", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Length", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Length", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "search_index", - "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": "Index", - "length": 0, - "no_copy": 0, - "oldfieldname": "search_index", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "search_index", + "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": "Index", + "length": 0, + "no_copy": 0, + "oldfieldname": "search_index", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "in_list_view", - "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": "In List View", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "70px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "in_list_view", + "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": "In List View", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "70px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "70px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "in_standard_filter", - "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": "In Standard Filter", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "in_standard_filter", + "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": "In Standard Filter", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", - "fieldname": "in_global_search", - "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": "In Global Search", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", + "fieldname": "in_global_search", + "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": "In Global Search", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bold", - "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": "Bold", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "bold", + "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": "Bold", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.fieldtype===\"Section Break\"", - "fieldname": "collapsible", - "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": "Collapsible", - "length": 255, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.fieldtype===\"Section Break\"", + "fieldname": "collapsible", + "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": "Collapsible", + "length": 255, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fieldname": "collapsible_depends_on", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Collapsible Depends On", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible_depends_on", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Collapsible Depends On", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", - "fieldname": "options", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Options", - "length": 0, - "no_copy": 0, - "oldfieldname": "options", - "oldfieldtype": "Text", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", + "fieldname": "options", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Options", + "length": 0, + "no_copy": 0, + "oldfieldname": "options", + "oldfieldtype": "Text", + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default", - "length": 0, - "no_copy": 0, - "oldfieldname": "default", - "oldfieldtype": "Text", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "default", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Default", + "length": 0, + "no_copy": 0, + "oldfieldname": "default", + "oldfieldtype": "Text", + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "permissions", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Permissions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "permissions", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Permissions", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "depends_on", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Display Depends On", - "length": 255, - "no_copy": 0, - "oldfieldname": "depends_on", - "oldfieldtype": "Data", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "depends_on", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Display Depends On", + "length": 255, + "no_copy": 0, + "oldfieldname": "depends_on", + "oldfieldtype": "Data", + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hidden", - "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": "Hidden", - "length": 0, - "no_copy": 0, - "oldfieldname": "hidden", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "hidden", + "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": "Hidden", + "length": 0, + "no_copy": 0, + "oldfieldname": "hidden", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "read_only", - "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": "Read Only", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "read_only", + "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": "Read Only", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "unique", - "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": "Unique", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "unique", + "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": "Unique", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Do not allow user to change after set the first time", - "fieldname": "set_only_once", - "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": "Set Only Once", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Do not allow user to change after set the first time", + "fieldname": "set_only_once", + "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": "Set Only Once", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.fieldtype == \"Table\"", - "fieldname": "allow_bulk_edit", - "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": "Allow Bulk Edit", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval: doc.fieldtype == \"Table\"", + "fieldname": "allow_bulk_edit", + "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": "Allow Bulk Edit", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_13", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_13", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "permlevel", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Perm Level", - "length": 0, - "no_copy": 0, - "oldfieldname": "permlevel", - "oldfieldtype": "Int", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Perm Level", + "length": 0, + "no_copy": 0, + "oldfieldname": "permlevel", + "oldfieldtype": "Int", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "User permissions should not apply for this Link", - "fieldname": "ignore_user_permissions", - "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": "Ignore User Permissions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "User permissions should not apply for this Link", + "fieldname": "ignore_user_permissions", + "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": "Ignore User Permissions", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: parent.is_submittable", - "fieldname": "allow_on_submit", - "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": "Allow on Submit", - "length": 0, - "no_copy": 0, - "oldfieldname": "allow_on_submit", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval: parent.is_submittable", + "fieldname": "allow_on_submit", + "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": "Allow on Submit", + "length": 0, + "no_copy": 0, + "oldfieldname": "allow_on_submit", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "report_hide", - "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": "Report Hide", - "length": 0, - "no_copy": 0, - "oldfieldname": "report_hide", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "report_hide", + "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": "Report Hide", + "length": 0, + "no_copy": 0, + "oldfieldname": "report_hide", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.fieldtype == 'Link')", - "fieldname": "remember_last_selected_value", - "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": "Remember Last Selected Value", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:(doc.fieldtype == 'Link')", + "fieldname": "remember_last_selected_value", + "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": "Remember Last Selected Value", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", - "fieldname": "ignore_xss_filter", - "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": "Ignore XSS Filter", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "fieldname": "ignore_xss_filter", + "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": "Ignore XSS Filter", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "display", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Display", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "display", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Display", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "in_filter", - "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": "In Filter", - "length": 0, - "no_copy": 0, - "oldfieldname": "in_filter", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "in_filter", + "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": "In Filter", + "length": 0, + "no_copy": 0, + "oldfieldname": "in_filter", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "no_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": "No Copy", - "length": 0, - "no_copy": 0, - "oldfieldname": "no_copy", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "no_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": "No Copy", + "length": 0, + "no_copy": 0, + "oldfieldname": "no_copy", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "print_hide", - "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": "Print Hide", - "length": 0, - "no_copy": 0, - "oldfieldname": "print_hide", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "print_hide", + "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": "Print Hide", + "length": 0, + "no_copy": 0, + "oldfieldname": "print_hide", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", - "fieldname": "print_hide_if_no_value", - "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": "Print Hide If No Value", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "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": "Print Hide If No Value", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "print_width", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Width", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "print_width", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Print Width", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "width", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Width", - "length": 0, - "no_copy": 0, - "oldfieldname": "width", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "50px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "width", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Width", + "length": 0, + "no_copy": 0, + "oldfieldname": "width", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", - "fieldname": "columns", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Columns", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Columns", + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_22", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_22", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Description", + "length": 0, + "no_copy": 0, + "oldfieldname": "description", + "oldfieldtype": "Text", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "300px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "oldfieldname", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "oldfieldname": "oldfieldname", - "oldfieldtype": "Data", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "oldfieldname", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "oldfieldname": "oldfieldname", + "oldfieldtype": "Data", + "permlevel": 0, + "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, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "oldfieldtype", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "oldfieldname": "oldfieldtype", - "oldfieldtype": "Data", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "oldfieldtype", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "oldfieldname": "oldfieldtype", + "oldfieldtype": "Data", + "permlevel": 0, + "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 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-10-07 19:20:15.888708", - "modified_by": "Administrator", - "module": "Core", - "name": "DocField", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-10-24 11:39:56.795852", + "modified_by": "Administrator", + "module": "Core", + "name": "DocField", + "owner": "Administrator", + "permissions": [], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_order": "ASC", + "track_changes": 0, "track_seen": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index be045fbb02..5c1aa65ca4 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import re, copy, os -import MySQLdb import frappe from frappe import _ @@ -17,6 +16,10 @@ from frappe.modules import make_boilerplate from frappe.model.db_schema import validate_column_name, validate_column_length import frappe.website.render +# imports - third-party imports +import pymysql +from pymysql.constants import ER + class InvalidFieldNameError(frappe.ValidationError): pass form_grid_templates = { @@ -291,6 +294,8 @@ class DocType(Document): `doctype` property for Single type.""" if self.issingle: frappe.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old)) + frappe.db.sql("""update tabSingles set value=%s + where doctype=%s and field='name' and value = %s""", (new, new, old)) else: frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new)) @@ -482,8 +487,8 @@ def validate_fields(meta): group by `{fieldname}` having count(*) > 1 limit 1""".format( doctype=d.parent, fieldname=d.fieldname)) - except MySQLdb.OperationalError as e: - if e.args and e.args[0]==1054: + except pymysql.InternalError as e: + if e.args and e.args[0] == ER.BAD_FIELD_ERROR: # ignore if missing column, else raise # this happens in case of Custom Field pass @@ -764,7 +769,8 @@ def validate_permissions(doctype, for_remove=False): def make_module_and_roles(doc, perm_fieldname="permissions"): """Make `Module Def` and `Role` records if already not made. Called while installing.""" try: - if doc.restrict_to_domain and not frappe.db.exists('Domain', doc.restrict_to_domain): + if hasattr(doc,'restrict_to_domain') and doc.restrict_to_domain and \ + not frappe.db.exists('Domain', doc.restrict_to_domain): frappe.get_doc(dict(doctype='Domain', domain=doc.restrict_to_domain)).insert() if not frappe.db.exists("Module Def", doc.module): diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py index 1f9ff60f66..61869f8fe8 100644 --- a/frappe/core/doctype/domain/domain.py +++ b/frappe/core/doctype/domain/domain.py @@ -18,10 +18,11 @@ class Domain(Document): self.setup_roles() self.setup_properties() self.set_values() - if not int(frappe.db.get_single_value('System Settings', 'setup_complete') or 0): + # always set the desktop icons while changing the domain settings + self.setup_desktop_icons() + if not int(frappe.defaults.get_defaults().setup_complete or 0): # if setup not complete, setup desktop etc. self.setup_sidebar_items() - self.setup_desktop_icons() self.set_default_portal_role() if self.data.custom_fields: @@ -59,7 +60,9 @@ class Domain(Document): def setup_roles(self): '''Enable roles that are restricted to this domain''' if self.data.restricted_roles: + user = frappe.get_doc("User", frappe.session.user) for role_name in self.data.restricted_roles: + user.append("roles", {"role": role_name}) if not frappe.db.get_value('Role', role_name): frappe.get_doc(dict(doctype='Role', role_name=role_name)).insert() continue @@ -67,6 +70,7 @@ class Domain(Document): role = frappe.get_doc('Role', role_name) role.disabled = 0 role.save() + user.save() def setup_data(self, domain=None): '''Load domain info via hooks''' @@ -97,6 +101,7 @@ class Domain(Document): '''set values based on `data.set_value`''' if self.data.set_value: for args in self.data.set_value: + frappe.reload_doctype(args[0]) doc = frappe.get_doc(args[0], args[1] or args[0]) doc.set(args[2], args[3]) doc.save() diff --git a/frappe/core/doctype/domain_settings/domain_settings.js b/frappe/core/doctype/domain_settings/domain_settings.js index 1750573111..1428727993 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.js +++ b/frappe/core/doctype/domain_settings/domain_settings.js @@ -2,58 +2,62 @@ // For license information, please see license.txt frappe.ui.form.on('Domain Settings', { - onload: function(frm) { - let domains = $('
') - .appendTo(frm.fields_dict.domains_html.wrapper); - - if(!frm.domain_editor) { - frm.domain_editor = new frappe.DomainsEditor(domains, frm); + before_load: function(frm) { + if(!frm.domains_multicheck) { + frm.domains_multicheck = frappe.ui.form.make_control({ + parent: frm.fields_dict.domains_html.$wrapper, + df: { + fieldname: "domains_multicheck", + fieldtype: "MultiCheck", + get_data: () => { + let active_domains = (frm.doc.active_domains || []).map(row => row.domain); + return frappe.boot.all_domains.map(domain => { + return { + label: domain, + value: domain, + checked: active_domains.includes(domain) + }; + }); + } + }, + render_input: true + }); + frm.domains_multicheck.refresh_input(); } - - frm.domain_editor.show(); }, validate: function(frm) { - if(frm.domain_editor) { - frm.domain_editor.set_items_in_table(); - } + frm.trigger('set_options_in_table'); }, -}); -frappe.DomainsEditor = frappe.CheckboxEditor.extend({ - init: function(wrapper, frm) { - var opts = {}; - $.extend(opts, { - wrapper: wrapper, - frm: frm, - field_mapper: { - child_table_field: "active_domains", - item_field: "domain", - cdt: "Has Domain" - }, - attribute: 'data-domain', - checkbox_selector: false, - get_items: this.get_all_domains, - editor_template: this.get_template() + set_options_in_table: function(frm) { + let selected_options = frm.domains_multicheck.get_value(); + let unselected_options = frm.domains_multicheck.options + .map(option => option.value) + .filter(value => { + return !selected_options.includes(value); + }); + + let map = {}, list = []; + (frm.doc.active_domains || []).map(row => { + map[row.domain] = row.name; + list.push(row.domain); }); - this._super(opts); - }, + unselected_options.map(option => { + if(list.includes(option)) { + frappe.model.clear_doc("Has Domain", map[option]); + } + }); - get_template: function() { - return ` -
- -
- `; - }, + selected_options.map(option => { + if(!list.includes(option)) { + frappe.model.clear_doc("Has Domain", map[option]); + let row = frappe.model.add_child(frm.doc, "Has Domain", "active_domains"); + row.domain = option; + } + }); - get_all_domains: function() { - // return all the domains available in the system - this.items = frappe.boot.all_domains; - this.render_items(); - }, -}); \ No newline at end of file + refresh_field('active_domains'); + } +}); diff --git a/frappe/core/doctype/domain_settings/domain_settings.json b/frappe/core/doctype/domain_settings/domain_settings.json index 9d65159099..8efd296da6 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.json +++ b/frappe/core/doctype/domain_settings/domain_settings.json @@ -18,7 +18,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "domains", + "fieldname": "active_domains_sb", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, @@ -27,7 +27,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Domains", + "label": "Active Domains", "length": 0, "no_copy": 0, "permlevel": 0, @@ -57,7 +57,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Domains", + "label": "Domains HTML", "length": 0, "no_copy": 0, "permlevel": 0, @@ -95,7 +95,7 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, @@ -114,7 +114,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-05-12 17:01:18.615000", + "modified": "2017-12-05 17:36:46.842134", "modified_by": "Administrator", "module": "Core", "name": "Domain Settings", diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py index ab3cfc38df..2ac9c716a7 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.py +++ b/frappe/core/doctype/domain_settings/domain_settings.py @@ -15,7 +15,10 @@ class DomainSettings(Document): self.save() def on_update(self): - for d in self.active_domains: + for i, d in enumerate(self.active_domains): + # set the flag to update the the desktop icons of all domains + if i >= 1: + frappe.flags.keep_desktop_icons = True domain = frappe.get_doc('Domain', d.domain) domain.setup_domain() diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index b311f51a84..d3275e6968 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, "autoname": "", @@ -11,6 +12,7 @@ "editable_grid": 0, "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -41,6 +43,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -71,6 +74,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -100,6 +104,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -129,6 +134,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -157,6 +163,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -187,6 +194,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -216,6 +224,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -244,6 +253,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -272,6 +282,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -301,6 +312,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -330,6 +342,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -360,6 +373,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -389,6 +403,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -418,6 +433,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -447,6 +463,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -475,6 +492,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -503,12 +521,13 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "content_hash", - "fieldtype": "Data", + "fieldname": "lft", + "fieldtype": "Int", "hidden": 1, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -516,26 +535,28 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Content Hash", + "label": "lft", "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": 1, + "search_index": 0, "set_only_once": 0, "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "lft", + "fieldname": "rgt", "fieldtype": "Int", "hidden": 1, "ignore_user_permissions": 0, @@ -544,7 +565,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "lft", + "label": "rgt", "length": 0, "no_copy": 0, "permlevel": 0, @@ -560,12 +581,13 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "rgt", - "fieldtype": "Int", + "fieldname": "old_parent", + "fieldtype": "Data", "hidden": 1, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -573,7 +595,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "rgt", + "label": "old_parent", "length": 0, "no_copy": 0, "permlevel": 0, @@ -589,20 +611,21 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "old_parent", + "fieldname": "content_hash", "fieldtype": "Data", - "hidden": 1, + "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": "old_parent", + "label": "Content Hash", "length": 0, "no_copy": 0, "permlevel": 0, @@ -618,19 +641,19 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-file", "idx": 1, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-02-17 16:42:36.092962", + "modified": "2017-10-27 13:27:43.882914", "modified_by": "Administrator", "module": "Core", "name": "File", diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 1976f4c862..03701e5ba0 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -423,3 +423,24 @@ def unzip_file(name): '''Unzip the given file and make file records for each of the extracted files''' file_obj = frappe.get_doc('File', name) file_obj.unzip() + +@frappe.whitelist() +def get_attached_images(doctype, names): + '''get list of image urls attached in form + returns {name: ['image.jpg', 'image.png']}''' + + if isinstance(names, string_types): + names = json.loads(names) + + img_urls = frappe.db.get_list('File', filters={ + 'attached_to_doctype': doctype, + 'attached_to_name': ('in', names), + 'is_folder': 0 + }, fields=['file_url', 'attached_to_name as docname']) + + out = frappe._dict() + for i in img_urls: + out[i.docname] = out.get(i.docname, []) + out[i.docname].append(i.file_url) + + return out \ No newline at end of file diff --git a/frappe/core/doctype/role_profile/__init__.py b/frappe/core/doctype/role_profile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/role_profile/role_profile.js b/frappe/core/doctype/role_profile/role_profile.js new file mode 100644 index 0000000000..09aead670a --- /dev/null +++ b/frappe/core/doctype/role_profile/role_profile.js @@ -0,0 +1,23 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Role Profile', { + setup: function(frm) { + if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) { + if(!frm.roles_editor) { + var role_area = $('
') + .appendTo(frm.fields_dict.roles_html.wrapper); + frm.roles_editor = new frappe.RoleEditor(role_area, frm); + frm.roles_editor.show(); + } else { + frm.roles_editor.show(); + } + } + }, + + validate: function(frm) { + if(frm.roles_editor) { + frm.roles_editor.set_roles_in_table(); + } + } +}); diff --git a/frappe/core/doctype/role_profile/role_profile.json b/frappe/core/doctype/role_profile/role_profile.json new file mode 100644 index 0000000000..4b3f35aa57 --- /dev/null +++ b/frappe/core/doctype/role_profile/role_profile.json @@ -0,0 +1,175 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "role_profile", + "beta": 0, + "creation": "2017-08-31 04:16:38.764465", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "role_profile", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Role Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 1 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "roles_html", + "fieldtype": "HTML", + "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": "Roles HTML", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "roles", + "fieldtype": "Table", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Roles Assigned", + "length": 0, + "no_copy": 0, + "options": "Has Role", + "permlevel": 1, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-10-17 11:05:11.183066", + "modified_by": "Administrator", + "module": "Core", + "name": "Role Profile", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "role_profile", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/role_profile/role_profile.py b/frappe/core/doctype/role_profile/role_profile.py new file mode 100644 index 0000000000..4def834adb --- /dev/null +++ b/frappe/core/doctype/role_profile/role_profile.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class RoleProfile(Document): + def autoname(self): + """set name as Role Profile name""" + self.name = self.role_profile + + def on_update(self): + """ Changes in role_profile reflected across all its user """ + from frappe.core.doctype.user.user import update_roles + update_roles(self.name) diff --git a/frappe/core/doctype/role_profile/test_role_profile.js b/frappe/core/doctype/role_profile/test_role_profile.js new file mode 100644 index 0000000000..559a5fc0ac --- /dev/null +++ b/frappe/core/doctype/role_profile/test_role_profile.js @@ -0,0 +1,33 @@ +QUnit.module('Core'); + +QUnit.test("test: Role Profile", function (assert) { + let done = assert.async(); + + assert.expect(3); + + frappe.run_serially([ + // insert a new user + () => frappe.tests.make('Role Profile', [ + {role_profile: 'Test 2'} + ]), + + () => { + $('input.box')[0].checked = true; + $('input.box')[2].checked = true; + $('input.box')[4].checked = true; + cur_frm.save(); + }, + + () => frappe.timeout(1), + () => cur_frm.refresh(), + () => frappe.timeout(2), + () => { + assert.equal($('input.box')[0].checked, true); + assert.equal($('input.box')[2].checked, true); + assert.equal($('input.box')[4].checked, true); + }, + + () => done() + ]); + +}); \ No newline at end of file diff --git a/frappe/core/doctype/role_profile/test_role_profile.py b/frappe/core/doctype/role_profile/test_role_profile.py new file mode 100644 index 0000000000..d338bec9e2 --- /dev/null +++ b/frappe/core/doctype/role_profile/test_role_profile.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest + +class TestRoleProfile(unittest.TestCase): + def test_make_new_role_profile(self): + new_role_profile = frappe.get_doc(dict(doctype='Role Profile', role_profile='Test 1')).insert() + + self.assertEquals(new_role_profile.role_profile, 'Test 1') + + # add role + new_role_profile.append("roles", { + "role": '_Test Role 2' + }) + new_role_profile.save() + self.assertEquals(new_role_profile.roles[0].role, '_Test Role 2') + + # clear roles + new_role_profile.roles = [] + new_role_profile.save() + self.assertEquals(new_role_profile.roles, []) \ No newline at end of file diff --git a/frappe/core/doctype/user/test_user.js b/frappe/core/doctype/user/test_user.js index 52f9b7e42c..923a39c3a5 100644 --- a/frappe/core/doctype/user/test_user.js +++ b/frappe/core/doctype/user/test_user.js @@ -20,4 +20,4 @@ QUnit.test("test: User", function (assert) { () => done() ]); -}); +}); \ No newline at end of file diff --git a/frappe/core/doctype/user/test_user_with_role_profile.js b/frappe/core/doctype/user/test_user_with_role_profile.js new file mode 100644 index 0000000000..5fd6f72410 --- /dev/null +++ b/frappe/core/doctype/user/test_user_with_role_profile.js @@ -0,0 +1,35 @@ +QUnit.module('Core'); + +QUnit.test("test: Set role profile in user", function (assert) { + let done = assert.async(); + + assert.expect(3); + + frappe.run_serially([ + + // Insert a new user + () => frappe.tests.make('User', [ + {email: 'test@test2.com'}, + {first_name: 'Test 2'}, + {send_welcome_email: 0} + ]), + + () => frappe.timeout(2), + () => { + return frappe.tests.set_form_values(cur_frm, [ + {role_profile_name:'Test 2'} + ]); + }, + + () => cur_frm.save(), + () => frappe.timeout(2), + + () => { + assert.equal($('input.box')[0].checked, true); + assert.equal($('input.box')[2].checked, true); + assert.equal($('input.box')[4].checked, true); + }, + () => done() + ]); + +}); diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index dbe9be60c1..c29b03a954 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -17,8 +17,30 @@ frappe.ui.form.on('User', { } }, + + role_profile_name: function(frm) { + if(frm.doc.role_profile_name) { + frappe.call({ + "method": "frappe.core.doctype.user.user.get_role_profile", + args: { + role_profile: frm.doc.role_profile_name + }, + callback: function (data) { + frm.set_value("roles", []); + $.each(data.message || [], function(i, v){ + var d = frm.add_child("roles"); + d.role = v.role; + }); + frm.roles_editor.show(); + } + }); + } + }, + onload: function(frm) { - if(has_common(frappe.user_roles, ["Administrator", "System Manager"]) && !frm.doc.__islocal) { + frm.can_edit_roles = has_common(frappe.user_roles, ["Administrator", "System Manager"]); + + if(frm.can_edit_roles && !frm.is_new()) { if(!frm.roles_editor) { var role_area = $('
') .appendTo(frm.fields_dict.roles_html.wrapper); @@ -34,7 +56,10 @@ frappe.ui.form.on('User', { }, refresh: function(frm) { var doc = frm.doc; - + if(!frm.is_new() && !frm.roles_editor && frm.can_edit_roles) { + frm.reload_doc(); + return; + } if(doc.name===frappe.session.user && !doc.__unsaved && frappe.all_timezones && (doc.language || frappe.boot.user.language) @@ -45,7 +70,7 @@ frappe.ui.form.on('User', { frm.toggle_display(['sb1', 'sb3', 'modules_access'], false); - if(!doc.__islocal){ + if(!frm.is_new()) { frm.add_custom_button(__("Set Desktop Icons"), function() { frappe.route_options = { "user": doc.name @@ -89,8 +114,8 @@ frappe.ui.form.on('User', { frm.trigger('enabled'); - if (frm.roles_editor) { - frm.roles_editor.disabled = frm.doc.role_profile_name ? 1 : 0; + if (frm.roles_editor && frm.can_edit_roles) { + frm.roles_editor.disable = frm.doc.role_profile_name ? 1 : 0; frm.roles_editor.show(); } @@ -133,13 +158,13 @@ frappe.ui.form.on('User', { }, enabled: function(frm) { var doc = frm.doc; - if(!doc.__islocal && has_common(frappe.user_roles, ["Administrator", "System Manager"])) { + if(!frm.is_new() && has_common(frappe.user_roles, ["Administrator", "System Manager"])) { frm.toggle_display(['sb1', 'sb3', 'modules_access'], doc.enabled); frm.set_df_property('enabled', 'read_only', 0); } if(frappe.session.user!=="Administrator") { - frm.toggle_enable('email', doc.__islocal); + frm.toggle_enable('email', frm.is_new()); } }, create_user_email:function(frm) { diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 5ce4df115f..984ca89b72 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -503,6 +503,37 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "role_profile_name", + "fieldtype": "Link", + "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": "Role Profile", + "length": 0, + "no_copy": 0, + "options": "Role Profile", + "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, @@ -922,6 +953,37 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "logout_all_sessions", + "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": "Logout from all devices while changing Password", + "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, @@ -1213,7 +1275,6 @@ "label": "Background Image", "length": 0, "no_copy": 0, - "options": "image", "permlevel": 0, "precision": "", "print_hide": 0, @@ -1389,7 +1450,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".", + "description": "Enter default value fields (keys) and values. If you add multiple values for a field,the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields,go to \"Customize Form\".", "fieldname": "defaults", "fieldtype": "Table", "hidden": 1, @@ -1483,7 +1544,7 @@ "collapsible": 0, "columns": 0, "default": "System User", - "description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop", + "description": "If the user has any role checked,then the user becomes a \"System User\". \"System User\" has access to the desktop", "fieldname": "user_type", "fieldtype": "Select", "hidden": 0, @@ -2002,8 +2063,8 @@ "istable": 0, "max_attachments": 5, "menu_index": 0, - "modified": "2017-10-09 15:33:43.818915", - "modified_by": "Administrator", + "modified": "2017-11-01 09:04:51.151347", + "modified_by": "manas@erpnext.com", "module": "Core", "name": "User", "owner": "Administrator", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 833beb5cc9..edf4c285b4 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -67,6 +67,7 @@ class User(Document): self.remove_disabled_roles() self.validate_user_email_inbox() ask_pass_update() + self.validate_roles() if self.language == "Loading...": self.language = None @@ -74,6 +75,12 @@ class User(Document): if (self.name not in ["Administrator", "Guest"]) and (not self.frappe_userid): self.frappe_userid = frappe.generate_hash(length=39) + def validate_roles(self): + if self.role_profile_name: + role_profile = frappe.get_doc('Role Profile', self.role_profile_name) + self.set('roles', []) + self.append_roles(*[role.role for role in role_profile.roles]) + def on_update(self): # clear new password self.validate_user_limit() @@ -84,6 +91,7 @@ class User(Document): if self.name not in ('Administrator', 'Guest') and not self.user_image: frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name) + def has_website_permission(self, ptype, verbose=False): """Returns true if current user is the session user""" return self.name == frappe.session.user @@ -137,7 +145,7 @@ class User(Document): def email_new_password(self, new_password=None): if new_password and not self.flags.in_insert: - _update_password(self.name, new_password) + _update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions) if self.send_password_update_notification: self.password_update_mail(new_password) @@ -183,7 +191,8 @@ class User(Document): if self.name not in STANDARD_USERS: if new_password: # new password given, no email required - _update_password(self.name, new_password) + _update_password(user=self.name, pwd=new_password, + logout_all_sessions=self.logout_all_sessions) if not self.flags.no_welcome_mail and self.send_welcome_email: self.send_welcome_mail_to_user() @@ -987,3 +996,17 @@ def throttle_user_creation(): if frappe.db.get_creation_count('User', 60) > frappe.local.conf.get("throttle_user_limit", 60): frappe.throw(_('Throttled')) + +@frappe.whitelist() +def get_role_profile(role_profile): + roles = frappe.get_doc('Role Profile', {'role_profile': role_profile}) + return roles.roles + +def update_roles(role_profile): + users = frappe.get_all('User', filters={'role_profile_name': role_profile}) + role_profile = frappe.get_doc('Role Profile', role_profile) + roles = [role.role for role in role_profile.roles] + for d in users: + user = frappe.get_doc('User', d) + user.set('roles', []) + user.add_roles(*roles) diff --git a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.js b/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.js index 1c1a6396c3..d5293ddfe1 100644 --- a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.js +++ b/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.js @@ -11,16 +11,16 @@ frappe.ui.form.on('User Permission for Page and Report', { if(!frm.roles_editor) { frm.role_area = $('
') .appendTo(frm.fields_dict.roles_html.wrapper); - frm.roles_editor = new frappe.RoleEditor(frm.role_area); + frm.roles_editor = new frappe.RoleEditor(frm.role_area, frm); } }, page: function(frm) { - frm.trigger("get_roles") + frm.trigger("get_roles"); }, report: function(frm){ - frm.trigger("get_roles") + frm.trigger("get_roles"); }, get_roles: function(frm) { @@ -30,26 +30,26 @@ frappe.ui.form.on('User Permission for Page and Report', { method:"get_custom_roles", doc: frm.doc, callback: function(r) { - refresh_field('roles') - frm.roles_editor.show() + refresh_field('roles'); + frm.roles_editor.show(); } - }) + }); }, update: function(frm) { if(frm.roles_editor) { - frm.roles_editor.set_roles_in_table() + frm.roles_editor.set_roles_in_table(); } return frappe.call({ method:"set_custom_roles", doc: frm.doc, callback: function(r) { - refresh_field('roles') - frm.roles_editor.show() - frappe.msgprint(__("Successfully Updated")) - frm.reload_doc() + refresh_field('roles'); + frm.roles_editor.show(); + frappe.msgprint(__("Successfully Updated")); + frm.reload_doc(); } - }) + }); } }); diff --git a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.json b/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.json index a7e057294f..040a136347 100644 --- a/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.json +++ b/frappe/core/doctype/user_permission_for_page_and_report/user_permission_for_page_and_report.json @@ -1,5 +1,6 @@ { "allow_copy": 1, + "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, "beta": 0, @@ -12,6 +13,7 @@ "engine": "InnoDB", "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -23,7 +25,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Set Role For", "length": 0, @@ -36,12 +38,13 @@ "read_only": 0, "remember_last_selected_value": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -73,6 +76,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -104,6 +108,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -133,6 +138,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -163,6 +169,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -193,6 +200,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -221,6 +229,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -251,17 +260,17 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 1, "idx": 0, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-02-22 18:07:29.954831", + "modified": "2017-12-21 04:24:24.963988", "modified_by": "Administrator", "module": "Core", "name": "User Permission for Page and Report", diff --git a/frappe/core/page/data_import_tool/__init__.py b/frappe/core/page/data_import_tool/__init__.py deleted file mode 100644 index 4dbcd0d163..0000000000 --- a/frappe/core/page/data_import_tool/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals diff --git a/frappe/core/page/data_import_tool/data_import_main.html b/frappe/core/page/data_import_tool/data_import_main.html deleted file mode 100644 index 549bebcdfd..0000000000 --- a/frappe/core/page/data_import_tool/data_import_main.html +++ /dev/null @@ -1,118 +0,0 @@ -
-
-

{%= __("Export Template") %}

-

{%= __("To import or update records, you must first download the template for importing.") %}

-
{%= __("Select Type of Document to Download") %}
-
- -
-
-
-
-

{{ __("1. Select Columns") }}

-

- - {%= __("Select All") %} - - {%= __("Select Mandatory") %} - - {%= __("Unselect All") %} -

-
-
-
-

{{ __("2. Download") }}

-
- -
-
{%= __("Recommended for inserting new records.") %}
-
-
-
- -
-
{%= __("Recommended bulk editing records via import, or understanding the import format.") %}
-
-
-
-
-
- -
-
-
-
-
-
-

{%= __("Import") %}

-

{%= __("Update the template and save in downloaded format before attaching.") %}

-
-
-
-

{{ __("1. Select File") }}

-
-
- -

{{ __("2. Upload") }}

-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-

- -

-
-
-

Import Log

-
-
-
-
-
diff --git a/frappe/core/page/data_import_tool/data_import_tool.css b/frappe/core/page/data_import_tool/data_import_tool.css deleted file mode 100644 index 67841d5014..0000000000 --- a/frappe/core/page/data_import_tool/data_import_tool.css +++ /dev/null @@ -1,7 +0,0 @@ -.data-import-tool { - padding: 15px; -} - -.data-import-tool hr { - margin: 10px -15px; -} diff --git a/frappe/core/page/data_import_tool/data_import_tool.js b/frappe/core/page/data_import_tool/data_import_tool.js deleted file mode 100644 index b7e9dab79a..0000000000 --- a/frappe/core/page/data_import_tool/data_import_tool.js +++ /dev/null @@ -1,233 +0,0 @@ - - -frappe.DataImportTool = Class.extend({ - init: function(parent) { - this.page = frappe.ui.make_app_page({ - parent: parent, - title: __("Data Import Tool"), - single_column: true - }); - this.page.add_inner_button(__("Help"), function() { - frappe.help.show_video("6wiriRKPhmg"); - }); - this.make(); - this.make_upload(); - }, - set_route_options: function() { - var doctype = null; - if(frappe.get_route()[1]) { - doctype = frappe.get_route()[1]; - } else if(frappe.route_options && frappe.route_options.doctype) { - doctype = frappe.route_options.doctype; - } - - if(in_list(frappe.boot.user.can_import, doctype)) { - this.select.val(doctype).change(); - } - - frappe.route_options = null; - }, - make: function() { - var me = this; - frappe.boot.user.can_import = frappe.boot.user.can_import.sort(); - - $(frappe.render_template("data_import_main", this)).appendTo(this.page.main); - - this.select = this.page.main.find("select.doctype"); - this.select_columns = this.page.main.find('.select-columns'); - this.select.on("change", function() { - me.doctype = $(this).val(); - frappe.model.with_doctype(me.doctype, function() { - me.page.main.find(".export-import-section").toggleClass(!!me.doctype); - if(me.doctype) { - - // render select columns - var parent_doctype = frappe.get_doc('DocType', me.doctype); - parent_doctype["reqd"] = true; - var doctype_list = [parent_doctype]; - - frappe.meta.get_table_fields(me.doctype).forEach(function(df) { - var d = frappe.get_doc('DocType', df.options); - d["reqd"]=df.reqd; - doctype_list.push(d); - }); - $(frappe.render_template("data_import_tool_columns", {doctype_list: doctype_list})) - .appendTo(me.select_columns.empty()); - } - }); - }); - - this.page.main.find('.btn-select-all').on('click', function() { - me.select_columns.find('.select-column-check').prop('checked', true); - }); - - this.page.main.find('.btn-unselect-all').on('click', function() { - me.select_columns.find('.select-column-check').prop('checked', false); - }); - - this.page.main.find('.btn-select-mandatory').on('click', function() { - me.select_columns.find('.select-column-check').prop('checked', false); - me.select_columns.find('.select-column-check[data-reqd="1"]').prop('checked', true); - }); - - var get_template_url = '/api/method/frappe.core.page.data_import_tool.exporter.get_template'; - - this.page.main.find(".btn-download-template").on('click', function() { - open_url_post(get_template_url, me.get_export_params(false)); - }); - - this.page.main.find(".btn-download-data").on('click', function() { - open_url_post(get_template_url, me.get_export_params(true)); - }); - - }, - get_export_params: function(with_data) { - var doctype = this.select.val(); - var columns = {}; - - this.select_columns.find('.select-column-check:checked').each(function() { - var _doctype = $(this).attr('data-doctype'); - var _fieldname = $(this).attr('data-fieldname'); - if(!columns[_doctype]) { - columns[_doctype] = []; - } - columns[_doctype].push(_fieldname); - }); - - return { - doctype: doctype, - parent_doctype: doctype, - select_columns: JSON.stringify(columns), - with_data: with_data ? 'Yes' : 'No', - all_doctypes: 'Yes', - from_data_import: 'Yes', - excel_format: this.page.main.find(".excel-check").is(":checked") ? 'Yes' : 'No' - } - }, - make_upload: function() { - var me = this; - frappe.upload.make({ - no_socketio: true, - parent: this.page.main.find(".upload-area"), - btn: this.page.main.find(".btn-import"), - get_params: function() { - return { - submit_after_import: me.page.main.find('[name="submit_after_import"]').prop("checked"), - ignore_encoding_errors: me.page.main.find('[name="ignore_encoding_errors"]').prop("checked"), - skip_errors: me.page.main.find('[name="skip_errors"]').prop("checked"), - overwrite: !me.page.main.find('[name="always_insert"]').prop("checked"), - update_only: me.page.main.find('[name="update_only"]').prop("checked"), - no_email: me.page.main.find('[name="no_email"]').prop("checked"), - from_data_import: 'Yes' - } - }, - args: { - method: 'frappe.core.page.data_import_tool.importer.upload', - }, - allow_multiple: 0, - onerror: function(r) { - me.onerror(r); - }, - queued: function() { - // async, show queued - msg_dialog.clear(); - frappe.msgprint(__("Import Request Queued. This may take a few moments, please be patient.")); - }, - running: function() { - // update async status as running - msg_dialog.clear(); - frappe.msgprint(__("Importing...")); - me.write_messages([__("Importing")]); - me.has_progress = false; - }, - progress: function(data) { - // show callback if async - if(data.progress) { - frappe.hide_msgprint(true); - me.has_progress = true; - frappe.show_progress(__("Importing"), data.progress[0], - data.progress[1]); - } - }, - callback: function(attachment, r) { - if(r.message.error || r.message.messages.length==0) { - me.onerror(r); - } else { - if(me.has_progress) { - frappe.show_progress(__("Importing"), 1, 1); - setTimeout(frappe.hide_progress, 1000); - } - - r.messages = ["
" + __("Import Successful!") + "
"]. - concat(r.message.messages) - - me.write_messages(r.messages); - } - }, - is_private: true - }); - - frappe.realtime.on("data_import_progress", function(data) { - if(data.progress) { - frappe.hide_msgprint(true); - me.has_progress = true; - frappe.show_progress(__("Importing"), data.progress[0], - data.progress[1]); - } - }) - - }, - write_messages: function(data) { - this.page.main.find(".import-log").removeClass("hide"); - var parent = this.page.main.find(".import-log-messages").empty(); - - // TODO render using template! - for (var i=0, l=data.length; i

').html(frappe.markdown(v)).appendTo(parent); - if(v.substr(0,5)=='Error') { - $p.css('color', 'red'); - } else if(v.substr(0,8)=='Inserted') { - $p.css('color', 'green'); - } else if(v.substr(0,7)=='Updated') { - $p.css('color', 'green'); - } else if(v.substr(0,5)=='Valid') { - $p.css('color', '#777'); - } else if(v.substr(0,7)=='Ignored') { - $p.css('color', '#777'); - } - } - }, - onerror: function(r) { - if(r.message) { - // bad design: moves r.messages to r.message.messages - r.messages = $.map(r.message.messages, function(v) { - var msg = v.replace("Inserted", "Valid") - .replace("Updated", "Valid").split("<"); - if (msg.length > 1) { - v = msg[0] + (msg[1].split(">").slice(-1)[0]); - } else { - v = msg[0]; - } - return v; - }); - - r.messages = ["

" + __("Import Failed") + "

"] - .concat(r.messages); - - r.messages.push("Please correct the format of the file and import again."); - - frappe.show_progress(__("Importing"), 1, 1); - - this.write_messages(r.messages); - } - } -}); - -frappe.pages['data-import-tool'].on_page_load = function(wrapper) { - frappe.data_import_tool = new frappe.DataImportTool(wrapper); -} - -frappe.pages['data-import-tool'].on_page_show = function(wrapper) { - frappe.data_import_tool && frappe.data_import_tool.set_route_options(); -} diff --git a/frappe/core/page/data_import_tool/data_import_tool.json b/frappe/core/page/data_import_tool/data_import_tool.json deleted file mode 100644 index f8e05ef598..0000000000 --- a/frappe/core/page/data_import_tool/data_import_tool.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "content": null, - "creation": "2012-06-14 15:07:25", - "docstatus": 0, - "doctype": "Page", - "icon": "fa fa-upload", - "idx": 1, - "modified": "2016-05-11 03:37:53.385693", - "modified_by": "Administrator", - "module": "Core", - "name": "data-import-tool", - "owner": "Administrator", - "page_name": "data-import-tool", - "roles": [], - "script": null, - "standard": "Yes", - "style": null, - "title": "Data Import Tool" -} \ No newline at end of file diff --git a/frappe/core/page/data_import_tool/data_import_tool_columns.html b/frappe/core/page/data_import_tool/data_import_tool_columns.html deleted file mode 100644 index 8b7575d47f..0000000000 --- a/frappe/core/page/data_import_tool/data_import_tool_columns.html +++ /dev/null @@ -1,22 +0,0 @@ -
-{% for doctype in doctype_list %} -
{{ doctype.name }}
-
- {% for f in doctype.fields %} - {% if (frappe.model.no_value_type.indexOf(f.fieldtype)===-1 && !f.hidden) %} - {% doctype.reqd||(f.reqd=0);%} -
-
- -
-
- {% endif %} - {% endfor %} -
-{% endfor %} -
diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index d18f7dd55c..7b95ba07a1 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -138,9 +138,6 @@ $.extend(frappe.desktop, { setup_wiggle: () => { // Wiggle, Wiggle, Wiggle. const DURATION_LONG_PRESS = 1000; - // lesser the antidode, more the wiggle (like your drunk uncle) - // 75 seems good to replicate the iOS feels. - const WIGGLE_ANTIDODE = 75; var timer_id = 0; const $cases = frappe.desktop.wrapper.find('.case-wrapper'); @@ -149,34 +146,29 @@ $.extend(frappe.desktop, { // This hack is so bad, I should punch myself. // Seriously, punch yourself. const text = $(object).find('.circle-text').html(); - + return text; })); - + const clearWiggle = () => { const $closes = $cases.find('.module-remove'); $closes.hide(); $notis.show(); - $icons.trigger('stopRumble'); + $icons.removeClass('wiggle'); frappe.desktop.wiggling = false; }; - // initiate wiggling. - $icons.jrumble({ - speed: WIGGLE_ANTIDODE // seems neat enough to match the iOS way - }); - frappe.desktop.wrapper.on('mousedown', '.app-icon', () => { timer_id = setTimeout(() => { frappe.desktop.wiggling = true; // hide all notifications. $notis.hide(); - + $cases.each((i) => { const $case = $($cases[i]); - const template = + const template = `
@@ -200,7 +192,7 @@ $.extend(frappe.desktop, { method: 'frappe.desk.doctype.desktop_icon.desktop_icon.hide', args: { name: name }, freeze: true, - callback: (response) => + callback: (response) => { if ( response.message ) { location.reload(); @@ -209,7 +201,7 @@ $.extend(frappe.desktop, { }) dialog.hide(); - + clearWiggle(); }); // Hacks, Hacks and Hacks. @@ -222,8 +214,9 @@ $.extend(frappe.desktop, { dialog.show(); }); }); - - $icons.trigger('startRumble'); + + $icons.addClass('wiggle'); + }, DURATION_LONG_PRESS); }); frappe.desktop.wrapper.on('mouseup mouseleave', '.app-icon', () => { diff --git a/frappe/core/page/modules_setup/modules_setup.html b/frappe/core/page/modules_setup/modules_setup.html index 48e186d37f..4bc70ef7c8 100644 --- a/frappe/core/page/modules_setup/modules_setup.html +++ b/frappe/core/page/modules_setup/modules_setup.html @@ -11,11 +11,11 @@
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index b0dc64de97..c986af35cc 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -155,7 +155,9 @@ frappe.PermissionEngine = Class.extend({ role: me.get_role() }, callback: function(r) { - me.render(r.message); + frappe.model.with_doc('DocType', me.get_doctype(), () => { + me.render(r.message); + }); } }); }, @@ -209,7 +211,10 @@ frappe.PermissionEngine = Class.extend({ var perm_cell = me.add_cell(row, d, "permissions").css("padding-top", 0); var perm_container = $("
").appendTo(perm_cell); - $.each(me.rights, function(i, r) { + const { is_submittable } = frappe.model.get_doc('DocType', me.get_doctype()); + + me.rights.forEach(r => { + if (!is_submittable && ['submit', 'cancel', 'amend'].includes(r)) return; me.add_check(perm_container, d, r); }); diff --git a/frappe/core/utils.py b/frappe/core/utils.py new file mode 100644 index 0000000000..01f18c16d0 --- /dev/null +++ b/frappe/core/utils.py @@ -0,0 +1,34 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe import _ + +def get_parent_doc(doc): + """Returns document of `reference_doctype`, `reference_doctype`""" + if not hasattr(doc, "parent_doc"): + if doc.reference_doctype and doc.reference_name: + doc.parent_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name) + else: + doc.parent_doc = None + return doc.parent_doc + +def set_timeline_doc(doc): + """Set timeline_doctype and timeline_name""" + parent_doc = get_parent_doc(doc) + if (doc.timeline_doctype and doc.timeline_name) or not parent_doc: + return + + timeline_field = parent_doc.meta.timeline_field + if not timeline_field: + return + + doctype = parent_doc.meta.get_link_doctype(timeline_field) + name = parent_doc.get(timeline_field) + + if doctype and name: + doc.timeline_doctype = doctype + doc.timeline_name = name + + else: + return \ No newline at end of file diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index a4bec7ec1f..16e9408d3f 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -220,7 +220,7 @@ "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", + "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -1161,7 +1161,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-07-06 17:23:43.835189", + "modified": "2017-10-24 11:40:37.986457", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/custom_field/test_custom_field.js b/frappe/custom/doctype/custom_field/test_custom_field.js new file mode 100644 index 0000000000..4ca743a395 --- /dev/null +++ b/frappe/custom/doctype/custom_field/test_custom_field.js @@ -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: Custom Field", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Custom Field + () => frappe.tests.make('Custom Field', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 8198f545da..227c6de1ee 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -176,6 +176,7 @@ frappe.customize_form.confirm = function(msg, frm) { frappe.msgprint(r.exc); } else { d.hide(); + frappe.show_alert({message:__('Customizations Reset'), indicator:'green'}); frappe.customize_form.clear_locals_and_refresh(frm); } } diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 70251711f3..abcccd1070 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -66,9 +66,9 @@ docfield_properties = { allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), ('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature'), ('Data', 'Select'), - ('Text', 'Small Text'), ('Text', 'Data', 'Barcode')) + ('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation')) -allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select',) +allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select', 'Data') class CustomizeForm(Document): def on_update(self): @@ -108,7 +108,7 @@ class CustomizeForm(Document): '''Create, update custom translation for this doctype''' current = self.get_name_translation() if current: - if self.label and current!=self.label: + if self.label and current.target_name != self.label: frappe.db.set_value('Translation', current.name, 'target_name', self.label) frappe.translate.clear_cache() else: @@ -163,16 +163,13 @@ class CustomizeForm(Document): property_type=doctype_properties[property]) for df in self.get("fields"): - if df.get("__islocal"): - continue - meta_df = meta.get("fields", {"fieldname": df.fieldname}) if not meta_df or meta_df[0].get("is_custom_field"): continue for property in docfield_properties: - if property != "idx" and df.get(property) != meta_df[0].get(property): + if property != "idx" and (df.get(property) or '') != (meta_df[0].get(property) or ''): if property == "fieldtype": self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property)) @@ -329,6 +326,6 @@ class CustomizeForm(Document): return frappe.db.sql("""delete from `tabProperty Setter` where doc_type=%s - and ifnull(field_name, '')!='naming_series'""", self.doc_type) + and !(`field_name`='naming_series' and `property`='options')""", self.doc_type) frappe.clear_cache(doctype=self.doc_type) self.fetch_to_customize() diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 8dc0a6a3fa..98a9fe0db3 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -94,7 +94,7 @@ "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nText\nText Editor\nTime", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nText\nText Editor\nTime", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -1202,7 +1202,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-10-11 06:45:20.172291", + "modified": "2017-10-24 11:41:31.075929", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/data_migration/doctype/data_migration_connector/connectors/postgres.py b/frappe/data_migration/doctype/data_migration_connector/connectors/postgres.py deleted file mode 100644 index 9c3e2af64d..0000000000 --- a/frappe/data_migration/doctype/data_migration_connector/connectors/postgres.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import unicode_literals -import frappe, psycopg2 -from .base import BaseConnection - -class PostGresConnection(BaseConnection): - def __init__(self, properties): - self.__dict__.update(properties) - self._connector = psycopg2.connect("host='{0}' dbname='{1}' user='{2}' password='{3}'".format(self.hostname, - self.database_name, self.username, self.password)) - self.cursor = self._connector.cursor() - - def get_objects(self, object_type, condition, selection): - if not condition: - condition = '' - else: - condition = ' WHERE ' + condition - self.cursor.execute('SELECT {0} FROM {1}{2}'.format(selection, object_type, condition)) - raw_data = self.cursor.fetchall() - data = [] - for r in raw_data: - row_dict = frappe._dict({}) - for i, value in enumerate(r): - row_dict[self.cursor.description[i][0]] = value - data.append(row_dict) - - return data - - def get_join_objects(self, object_type, field, primary_key): - """ - field.formula 's first line will be list of tables that needs to be linked to fetch an item - The subsequent lines that follows will contain one to one mapping across tables keys - """ - condition = "" - key_mapping = field.formula.split('\n') - obj_type = key_mapping[0] - selection = field.source_fieldname - - for d in key_mapping[1:]: - condition += d + ' AND ' - - condition += str(object_type) + ".id=" + str(primary_key) - - return self.get_objects(obj_type, condition, selection) diff --git a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.json b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.json index e4aca6763d..338d59aadd 100644 --- a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.json +++ b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.json @@ -62,7 +62,7 @@ "label": "Connector Type", "length": 0, "no_copy": 0, - "options": "\nFrappe\nPostgres\nCustom", + "options": "\nFrappe\nCustom", "permlevel": 0, "precision": "", "print_hide": 0, @@ -268,7 +268,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-10-26 12:03:40.646348", + "modified": "2017-12-01 13:38:55.992499", "modified_by": "Administrator", "module": "Data Migration", "name": "Data Migration Connector", diff --git a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py index 5c597ee689..793dfe6694 100644 --- a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py +++ b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py @@ -8,7 +8,6 @@ from frappe.model.document import Document from frappe import _ from frappe.modules.export_file import create_init_py from .connectors.base import BaseConnection -from .connectors.postgres import PostGresConnection from .connectors.frappe_connection import FrappeConnection class DataMigrationConnector(Document): @@ -27,10 +26,7 @@ class DataMigrationConnector(Document): _class = get_connection_class(self.python_module) return _class(self) else: - if self.connector_type == 'Frappe': - self.connection = FrappeConnection(self) - elif self.connector_type == 'PostGres': - self.connection = PostGresConnection(self.as_dict()) + self.connection = FrappeConnection(self) return self.connection diff --git a/frappe/database.py b/frappe/database.py index 1092636461..87623a93e1 100644 --- a/frappe/database.py +++ b/frappe/database.py @@ -5,9 +5,6 @@ # -------------------- from __future__ import unicode_literals -import MySQLdb -from MySQLdb.times import DateTimeDeltaType -from markdown2 import UnicodeWithAttrs import warnings import datetime import frappe @@ -17,11 +14,25 @@ import re import frappe.model.meta from frappe.utils import now, get_datetime, cstr from frappe import _ -from six import text_type, binary_type, string_types, integer_types from frappe.model.utils.link_count import flush_local_link_count -from six import iteritems, text_type from frappe.utils.background_jobs import execute_job, get_queue +# imports - compatibility imports +from six import ( + integer_types, + string_types, + binary_type, + text_type, + iteritems +) + +# imports - third-party imports +from markdown2 import UnicodeWithAttrs +from pymysql.times import TimeDelta +from pymysql.constants import ER, FIELD_TYPE +from pymysql.converters import conversions +import pymysql + class Database: """ Open a database connection with the given parmeters, if use_default is True, use the @@ -50,7 +61,7 @@ class Database: def connect(self): """Connects to a database as set in `site_config.json`.""" - warnings.filterwarnings('ignore', category=MySQLdb.Warning) + warnings.filterwarnings('ignore', category=pymysql.Warning) usessl = 0 if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key: usessl = 1 @@ -59,19 +70,23 @@ class Database: 'cert':frappe.conf.db_ssl_cert, 'key':frappe.conf.db_ssl_key } + + conversions.update({ + FIELD_TYPE.NEWDECIMAL: float, + FIELD_TYPE.DATETIME: get_datetime, + TimeDelta: conversions[binary_type], + UnicodeWithAttrs: conversions[text_type] + }) + if usessl: - self._conn = MySQLdb.connect(self.host, self.user or '', self.password or '', - use_unicode=True, charset='utf8mb4', ssl=self.ssl) + self._conn = pymysql.connect(self.host, self.user or '', self.password or '', + charset='utf8mb4', use_unicode = True, ssl=self.ssl, conv = conversions) else: - self._conn = MySQLdb.connect(self.host, self.user or '', self.password or '', - use_unicode=True, charset='utf8mb4') - self._conn.converter[246]=float - self._conn.converter[12]=get_datetime - self._conn.encoders[UnicodeWithAttrs] = self._conn.encoders[text_type] - self._conn.encoders[DateTimeDeltaType] = self._conn.encoders[binary_type] + self._conn = pymysql.connect(self.host, self.user or '', self.password or '', + charset='utf8mb4', use_unicode = True, conv = conversions) - MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1 - self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF) + # MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1 + # # self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF) self._cursor = self._conn.cursor() if self.user != 'root': @@ -142,7 +157,6 @@ class Database: frappe.errprint(query % values) except TypeError: frappe.errprint([query, values]) - if (frappe.conf.get("logging") or False)==2: frappe.log("<<<< query") frappe.log(query) @@ -150,7 +164,6 @@ class Database: frappe.log(values) frappe.log(">>>>") self._cursor.execute(query, values) - else: if debug: self.explain_query(query) @@ -163,8 +176,8 @@ class Database: self._cursor.execute(query) except Exception as e: - # ignore data definition errors - if ignore_ddl and e.args[0] in (1146,1054,1091): + if ignore_ddl and e.args[0] in (ER.BAD_FIELD_ERROR, ER.NO_SUCH_TABLE, + ER.CANT_DROP_FIELD_OR_KEY): pass # NOTE: causes deadlock @@ -175,7 +188,6 @@ class Database: # as_dict=as_dict, as_list=as_list, formatted=formatted, # debug=debug, ignore_ddl=ignore_ddl, as_utf8=as_utf8, # auto_commit=auto_commit, update=update) - else: raise @@ -861,7 +873,7 @@ class Database: def close(self): """Close database connection.""" if self._conn: - self._cursor.close() + # self._cursor.close() self._conn.close() self._cursor = None self._conn = None @@ -871,7 +883,7 @@ class Database: if isinstance(s, text_type): s = (s or "").encode("utf-8") - s = text_type(MySQLdb.escape_string(s), "utf-8").replace("`", "\\`") + s = text_type(pymysql.escape_string(s), "utf-8").replace("`", "\\`") # NOTE separating % escape, because % escape should only be done when using LIKE operator # or when you use python format string to generate query that already has a %s diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py index d9cd03004a..ac7bd2ee1a 100644 --- a/frappe/desk/calendar.py +++ b/frappe/desk/calendar.py @@ -24,3 +24,23 @@ def get_event_conditions(doctype, filters=None): frappe.throw(_("Not Permitted"), frappe.PermissionError) return get_filters_cond(doctype, filters, [], with_match_conditions = True) + +@frappe.whitelist() +def get_events(doctype, start, end, field_map, filters=None, fields=None): + field_map = frappe._dict(json.loads(field_map)) + + if filters: + filters = json.loads(filters or '') + + if not fields: + fields = [field_map.start, field_map.end, field_map.title, 'name'] + + start_date = "ifnull(%s, '0000-00-00 00:00:00')" % field_map.start + end_date = "ifnull(%s, '2199-12-31 00:00:00')" % field_map.end + + filters += [ + [doctype, start_date, '<=', end], + [doctype, end_date, '>=', start], + ] + + return frappe.get_list(doctype, fields=fields, filters=filters) diff --git a/frappe/desk/doctype/calendar_view/__init__.py b/frappe/desk/doctype/calendar_view/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/calendar_view/calendar_view.js b/frappe/desk/doctype/calendar_view/calendar_view.js new file mode 100644 index 0000000000..a58a9555db --- /dev/null +++ b/frappe/desk/doctype/calendar_view/calendar_view.js @@ -0,0 +1,35 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Calendar View', { + onload: function(frm) { + frm.trigger('reference_doctype'); + }, + refresh: function(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__('Show Calendar'), + () => frappe.set_route('List', frm.doc.reference_doctype, 'Calendar', frm.doc.name)); + } + }, + reference_doctype: function(frm) { + const { reference_doctype } = frm.doc; + if (!reference_doctype) return; + + frappe.model.with_doctype(reference_doctype, () => { + const meta = frappe.get_meta(reference_doctype); + + const subject_options = meta.fields.filter( + df => !frappe.model.no_value_type.includes(df.fieldtype) + ).map(df => df.fieldname); + + const date_options = meta.fields.filter( + df => ['Date', 'Datetime'].includes(df.fieldtype) + ).map(df => df.fieldname); + + frm.set_df_property('subject_field', 'options', subject_options); + frm.set_df_property('start_date_field', 'options', date_options); + frm.set_df_property('end_date_field', 'options', date_options); + frm.refresh(); + }); + } +}); diff --git a/frappe/desk/doctype/calendar_view/calendar_view.json b/frappe/desk/doctype/calendar_view/calendar_view.json new file mode 100644 index 0000000000..227aa90f75 --- /dev/null +++ b/frappe/desk/doctype/calendar_view/calendar_view.json @@ -0,0 +1,204 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "Prompt", + "beta": 0, + "creation": "2017-10-23 13:02:10.295824", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subject_field", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Subject Field", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "start_date_field", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Start Date Field", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "end_date_field", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "End Date Field", + "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 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-11-14 14:14:11.544811", + "modified_by": "Administrator", + "module": "Desk", + "name": "Calendar View", + "name_case": "", + "owner": "faris@erpnext.com", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "All", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/desk/doctype/calendar_view/calendar_view.py b/frappe/desk/doctype/calendar_view/calendar_view.py new file mode 100644 index 0000000000..ae8ab1eb46 --- /dev/null +++ b/frappe/desk/doctype/calendar_view/calendar_view.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class CalendarView(Document): + pass diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 91de421e8f..6d5e2d6ab5 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -196,11 +196,13 @@ def set_desktop_icons(visible_list, ignore_duplicate=True): if the desktop icon does not exist and the name is a DocType, then will create an icon for the doctype''' - # clear all custom - frappe.db.sql('delete from `tabDesktop Icon` where standard=0') + # clear all custom only if setup is not complete + if not int(frappe.defaults.get_defaults().setup_complete or 0): + frappe.db.sql('delete from `tabDesktop Icon` where standard=0') - # set all as blocked - frappe.db.sql('update `tabDesktop Icon` set blocked=0, hidden=1') + # set standard as blocked and hidden if setting first active domain + if not frappe.flags.keep_desktop_icons: + frappe.db.sql('update `tabDesktop Icon` set blocked=0, hidden=1 where standard=1') # set as visible if present, or add icon for module_name in visible_list: diff --git a/frappe/desk/doctype/todo/todo.js b/frappe/desk/doctype/todo/todo.js index 8a13103311..0317281371 100644 --- a/frappe/desk/doctype/todo/todo.js +++ b/frappe/desk/doctype/todo/todo.js @@ -18,7 +18,7 @@ frappe.ui.form.on("ToDo", { } if (!frm.doc.__islocal) { - if(frm.doc.status=="Open") { + if(frm.doc.status!=="Closed") { frm.add_custom_button(__("Close"), function() { frm.set_value("status", "Closed"); frm.save(null, function() { @@ -27,7 +27,7 @@ frappe.ui.form.on("ToDo", { }); }, "fa fa-check", "btn-success"); } else { - frm.add_custom_button(__("Re-open"), function() { + frm.add_custom_button(__("Reopen"), function() { frm.set_value("status", "Open"); frm.save(); }, null, "btn-default"); diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index ee7fffa242..b09a23cbb4 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -94,6 +94,7 @@ def get_docinfo(doc=None, doctype=None, name=None): frappe.response["docinfo"] = { "attachments": get_attachments(doc.doctype, doc.name), "communications": _get_communications(doc.doctype, doc.name), + 'total_comments': len(json.loads(doc.get('_comments') or '[]')), 'versions': get_versions(doc), "assignments": get_assignments(doc.doctype, doc.name), "permissions": get_doc_permissions(doc), diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 999a83a2fe..683791534d 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -59,6 +59,17 @@ def add_comment(doc): return doc.as_dict() +@frappe.whitelist() +def update_comment(name, content): + """allow only owner to update comment""" + doc = frappe.get_doc('Communication', name) + + if frappe.session.user not in ['Administrator', doc.owner]: + frappe.throw(_('Comment can only be edited by the owner'), frappe.PermissionError) + + doc.content = content + doc.save(ignore_permissions=True) + @frappe.whitelist() def get_next(doctype, value, prev, filters=None, order_by="modified desc"): diff --git a/frappe/desk/page/activity/activity.js b/frappe/desk/page/activity/activity.js index 97e81c86d4..35bb76ad8c 100644 --- a/frappe/desk/page/activity/activity.js +++ b/frappe/desk/page/activity/activity.js @@ -78,12 +78,12 @@ frappe.pages['activity'].on_page_load = function(wrapper) { }, 'fa fa-th') } - this.page.add_menu_item(__('Authentication Log'), function() { + this.page.add_menu_item(__('Activity Log'), function() { frappe.route_options = { "user": frappe.session.user } - frappe.set_route('Report', "Authentication Log"); + frappe.set_route('Report', "Activity Log"); }, 'fa fa-th') this.page.add_menu_item(__('Show Likes'), function() { @@ -180,12 +180,14 @@ frappe.activity.render_heatmap = function(page) { method: "frappe.desk.page.activity.activity.get_heatmap_data", callback: function(r) { if(r.message) { - var heatmap = new frappe.ui.HeatMap({ - parent: $(".heatmap"), + var heatmap = new Chart({ + parent: ".heatmap", + type: 'heatmap', height: 100, start: new Date(moment().subtract(1, 'year').toDate()), count_label: "actions", - discrete_domains: 0 + discrete_domains: 0, + data: {} }); heatmap.update(r.message); diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py index 94b0296b6f..54d643ade2 100644 --- a/frappe/desk/page/activity/activity.py +++ b/frappe/desk/page/activity/activity.py @@ -4,25 +4,31 @@ from __future__ import unicode_literals import frappe from frappe.utils import cint -from frappe.core.doctype.communication.feed import get_feed_match_conditions +from frappe.core.doctype.activity_log.feed import get_feed_match_conditions @frappe.whitelist() def get_feed(start, page_length, show_likes=False): """get feed""" match_conditions = get_feed_match_conditions(frappe.session.user) - result = frappe.db.sql("""select name, owner, modified, creation, seen, comment_type, + result = frappe.db.sql("""select X.* + from (select name, owner, modified, creation, seen, comment_type, reference_doctype, reference_name, link_doctype, link_name, subject, communication_type, communication_medium, content - from `tabCommunication` - where + from `tabCommunication` + where communication_type in ("Communication", "Comment") and communication_medium != "Email" and (comment_type is null or comment_type != "Like" or (comment_type="Like" and (owner=%(user)s or reference_owner=%(user)s))) {match_conditions} {show_likes} - order by creation desc + union + select name, owner, modified, creation, '0', 'Updated', + reference_doctype, reference_name, link_doctype, link_name, subject, + 'Comment', '', content + from `tabActivity Log`) X + order by X.creation DESC limit %(start)s, %(page_length)s""" .format(match_conditions="and {0}".format(match_conditions) if match_conditions else "", show_likes="and comment_type='Like'" if show_likes else ""), @@ -43,10 +49,8 @@ def get_feed(start, page_length, show_likes=False): @frappe.whitelist() def get_heatmap_data(): return dict(frappe.db.sql("""select unix_timestamp(date(creation)), count(name) - from `tabCommunication` + from `tabActivity Log` where - communication_type in ("Communication", "Comment") - and communication_medium != "Email" - and date(creation) > subdate(curdate(), interval 1 year) + date(creation) > subdate(curdate(), interval 1 year) group by date(creation) order by creation asc""")) \ No newline at end of file diff --git a/frappe/desk/page/modules/modules.js b/frappe/desk/page/modules/modules.js index 02806396c2..4ba400fd4b 100644 --- a/frappe/desk/page/modules/modules.js +++ b/frappe/desk/page/modules/modules.js @@ -26,19 +26,40 @@ frappe.pages['modules'].on_page_load = function(wrapper) { }); } + page.get_page_modules = () => { + return frappe.get_desktop_icons(true) + .filter(d => d.type==='module' && !d.blocked) + .sort((a, b) => { return (a._label > b._label) ? 1 : -1; }); + }; + + let get_module_sidebar_item = (item) => `
  • + + + ${item._label} + +
  • `; + + let get_sidebar_html = () => { + let sidebar_items_html = page.get_page_modules() + .map(get_module_sidebar_item.bind(this)).join(""); + + return ``; + }; + // render sidebar - page.sidebar.html(frappe.render_template('modules_sidebar', - {modules: frappe.get_desktop_icons(true).sort( - function(a, b){ return (a._label > b._label) ? 1 : -1 })})); + page.sidebar.html(get_sidebar_html()); // help click - page.main.on("click", '.module-section-link[data-type="help"]', function(event) { + page.main.on("click", '.module-section-link[data-type="help"]', function() { frappe.help.show_video($(this).attr("data-youtube-id")); return false; }); // notifications click - page.main.on("click", '.open-notification', function(event) { + page.main.on("click", '.open-notification', function() { var doctype = $(this).attr('data-doctype'); if(doctype) { frappe.ui.notifications.show_open_count_list(doctype); @@ -50,9 +71,10 @@ frappe.pages['modules'].on_page_load = function(wrapper) { page.wrapper.find('.module-sidebar-item.active, .module-link.active').removeClass('active'); $(link).addClass('active').parent().addClass("active"); show_section($(link).attr('data-name')); - } + }; var show_section = function(module_name) { + if (!module_name) return; if(module_name in page.section_data) { render_section(page.section_data[module_name]); } else { @@ -73,7 +95,7 @@ frappe.pages['modules'].on_page_load = function(wrapper) { }); } - } + }; var render_section = function(m) { page.set_title(__(m.label)); @@ -88,7 +110,7 @@ frappe.pages['modules'].on_page_load = function(wrapper) { //setup_section_toggle(); frappe.app.update_notification_count_in_modules(); - } + }; var process_data = function(module_name, data) { frappe.module_links[module_name] = []; @@ -103,7 +125,7 @@ frappe.pages['modules'].on_page_load = function(wrapper) { } if(!item.route) { if(item.link) { - item.route=strip(item.link, "#") + item.route=strip(item.link, "#"); } else if(item.type==="doctype") { if(frappe.model.is_single(item.doctype)) { @@ -112,16 +134,16 @@ frappe.pages['modules'].on_page_load = function(wrapper) { if (item.filters) { frappe.route_options=item.filters; } - item.route="List/" + item.doctype + item.route="List/" + item.doctype; //item.style = 'font-weight: 500;'; } // item.style = 'font-weight: bold;'; } else if(item.type==="report" && item.is_query_report) { - item.route="query-report/" + item.name + item.route="query-report/" + item.name; } else if(item.type==="report") { - item.route="Report/" + item.doctype + "/" + item.name + item.route="Report/" + item.doctype + "/" + item.name; } else if(item.type==="page") { item.route=item.name; @@ -130,7 +152,7 @@ frappe.pages['modules'].on_page_load = function(wrapper) { if(item.route_options) { item.route += "?" + $.map(item.route_options, function(value, key) { - return encodeURIComponent(key) + "=" + encodeURIComponent(value) }).join('&') + return encodeURIComponent(key) + "=" + encodeURIComponent(value); }).join('&'); } if(item.type==="page" || item.type==="help" || item.type==="report" || @@ -139,22 +161,28 @@ frappe.pages['modules'].on_page_load = function(wrapper) { } }); }); - } -} + }; +}; frappe.pages['modules'].on_page_show = function(wrapper) { - var route = frappe.get_route(); + let route = frappe.get_route(); + let modules = frappe.modules_page.get_page_modules().map(d => d.module_name); $("body").attr("data-sidebar", 1); if(route.length > 1) { // activate section based on route - frappe.modules_page.activate_link( - frappe.modules_page.sidebar.find('.module-link[data-name="'+ route[1] +'"]')); + let module_name = route[1]; + if(modules.includes(module_name)) { + frappe.modules_page.activate_link( + frappe.modules_page.sidebar.find('.module-link[data-name="'+ module_name +'"]')); + } else { + frappe.throw(__(`Module ${module_name} not found.`)); + } } else if(frappe.modules_page.last_link) { // open last link - frappe.set_route('modules', frappe.modules_page.last_link.attr('data-name')) + frappe.set_route('modules', frappe.modules_page.last_link.attr('data-name')); } else { // first time, open the first page frappe.modules_page.activate_link(frappe.modules_page.sidebar.find('.module-link:first')); } -} +}; diff --git a/frappe/desk/page/modules/modules_sidebar.html b/frappe/desk/page/modules/modules_sidebar.html deleted file mode 100644 index 3d641c6235..0000000000 --- a/frappe/desk/page/modules/modules_sidebar.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/frappe/desk/page/modules/modules_sidebar_item.html b/frappe/desk/page/modules/modules_sidebar_item.html deleted file mode 100644 index 4e4b2c7720..0000000000 --- a/frappe/desk/page/modules/modules_sidebar_item.html +++ /dev/null @@ -1,7 +0,0 @@ -
  • - - - {{ item._label }} -
  • diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 12ccc69cc3..3b75c5cba4 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -7,6 +7,7 @@ frappe.setup = { events: {}, data: {}, utils: {}, + domains: [], on: function(event, fn) { if(!frappe.setup.events[event]) { @@ -26,7 +27,8 @@ frappe.setup = { } frappe.pages['setup-wizard'].on_page_load = function(wrapper) { - var requires = (frappe.boot.setup_wizard_requires || []); + let requires = (frappe.boot.setup_wizard_requires || []); + frappe.require(requires, function() { frappe.call({ @@ -180,46 +182,79 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } action_on_complete() { - var me = this; if (!this.current_slide.set_values()) return; this.update_values(); this.show_working_state(); this.disable_keyboard_nav(); + this.listen_for_setup_stages(); + return frappe.call({ method: "frappe.desk.page.setup_wizard.setup_wizard.setup_complete", args: {args: this.values}, - callback: function() { - me.show_setup_complete_state(); - if(frappe.setup.welcome_page) { - localStorage.setItem("session_last_route", frappe.setup.welcome_page); + callback: (r) => { + if(r.message.status === 'ok') { + this.post_setup_success(); + } else if(r.message.fail !== undefined) { + this.abort_setup(r.message.fail); } - setTimeout(function() { - // Reload - window.location.href = ''; - }, 2000); - setTimeout(()=> { - $('body').removeClass('setup-state'); - }, 20000); }, - error: function() { - var d = frappe.msgprint(__("There were errors.")); - d.custom_onhide = function() { - $(me.parent).find('.page-card-container').remove(); - $('body').removeClass('setup-state'); - me.container.show(); - frappe.set_route(me.page_name, me.slides.length - 1); - }; - } + error: this.abort_setup.bind(this, "Error in setup", true) }); } + post_setup_success() { + this.set_setup_complete_message(__("Setup Complete"), __("Refreshing...")); + if(frappe.setup.welcome_page) { + localStorage.setItem("session_last_route", frappe.setup.welcome_page); + } + setTimeout(function() { + // Reload + window.location.href = ''; + }, 2000); + } + + abort_setup(fail_msg, error=false) { + this.$working_state.find('.state-icon-container').html(''); + fail_msg = fail_msg ? fail_msg : __("Failed to complete setup"); + + if(error && !frappe.boot.developer_mode) { + frappe.msgprint(`Don't worry. It's not you, it's us. We've + received the issue details and will get back to you on the solution. + Please feel free to contact us on support@erpnext.com in the meantime.`); + } + + this.update_setup_message('Could not start up: ' + fail_msg); + + this.$working_state.find('.title').html('Setup failed'); + + this.$abort_btn.show(); + } + + listen_for_setup_stages() { + frappe.realtime.on("setup_task", (data) => { + // console.log('data', data); + if(data.stage_status) { + // .html('Process '+ data.progress[0] + ' of ' + data.progress[1] + ': ' + data.stage_status); + this.update_setup_message(data.stage_status); + this.set_setup_load_percent((data.progress[0]+1)/data.progress[1] * 100); + } + if(data.fail_msg) { + this.abort_setup(data.fail_msg); + } + }) + } + + update_setup_message(message) { + this.$working_state.find('.setup-message').html(message); + } + get_setup_slides_filtered_by_domain() { var filtered_slides = []; frappe.setup.slides.forEach(function(slide) { - if(frappe.setup.domain) { - var domains = slide.domains; - if (domains.indexOf('all') !== -1 || - domains.indexOf(frappe.setup.domain.toLowerCase()) !== -1) { + if(frappe.setup.domains) { + let active_domains = frappe.setup.domains; + if (!slide.domains || + slide.domains.filter(d => active_domains.includes(d)).length > 0) { filtered_slides.push(slide); } } else { @@ -231,51 +266,56 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { show_working_state() { this.container.hide(); - $('body').addClass('setup-state'); frappe.set_route(this.page_name); - this.working_state_message = this.get_message( - __("Setting Up"), - __("Sit tight while your system is being setup. This may take a few moments."), - true - ).appendTo(this.parent); + this.$working_state = this.get_message( + __("Setting up your system"), + __("Starting Frappé ...")).appendTo(this.parent); + + this.attach_abort_button(); this.current_id = this.slides.length; this.current_slide = null; - this.completed_state_message = this.get_message( - __("Setup Complete"), - __("You're all set!") - ); } - show_setup_complete_state() { - this.working_state_message.hide(); - this.completed_state_message.appendTo(this.parent); + attach_abort_button() { + this.$abort_btn = $(``); + this.$working_state.find('.content').append(this.$abort_btn); + + this.$abort_btn.on('click', () => { + $(this.parent).find('.setup-in-progress').remove(); + this.container.show(); + frappe.set_route(this.page_name, this.slides.length - 1); + }); + + this.$abort_btn.hide(); } - get_message(title, message="", loading=false) { - const loading_html = loading - ? '
    ' - : `
    - -
    `; - - return $(`
    -
    -
    - ${loading - ? `${title}` - : `${title}` - } -
    -

    ${message}

    -
    - ${loading_html} -
    + get_message(title, message="") { + const loading_html = `
    +
    +
    +
    +
    `; + + return $(`
    +
    +

    ${title}

    +
    ${loading_html}
    +

    ${message}

    `); } + + set_setup_complete_message(title, message) { + this.$working_state.find('.title').html(title); + this.$working_state.find('.setup-message').html(message); + } + + set_setup_load_percent(percent) { + this.$working_state.find('.progress-bar').css({"width": percent + "%"}); + } }; frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide { @@ -311,7 +351,6 @@ frappe.setup.slides_settings = [ { // Welcome (language) slide name: "welcome", - domains: ["all"], title: __("Hello!"), icon: "fa fa-world", // help: __("Let's prepare the system for first use."), @@ -344,7 +383,6 @@ frappe.setup.slides_settings = [ { // Region slide name: 'region', - domains: ["all"], title: __("Select Your Region"), icon: "fa fa-flag", // help: __("Select your Country, Time Zone and Currency"), @@ -376,7 +414,6 @@ frappe.setup.slides_settings = [ { // Profile slide name: 'user', - domains: ["all"], title: __("The First User: You"), icon: "fa fa-user", fields: [ diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 9f7d194d22..eef203ab04 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -13,50 +13,117 @@ from werkzeug.useragents import UserAgent from . import install_fixtures from six import string_types +def get_setup_stages(args): + + # App setup stage functions should not include frappe.db.commit + # That is done by frappe after successful completion of all stages + stages = [ + { + 'status': 'Updating global settings', + 'fail_msg': 'Failed to update global settings', + 'tasks': [ + { + 'fn': update_global_settings, + 'args': args, + 'fail_msg': 'Failed to update global settings' + } + ] + } + ] + + stages += get_stages_hooks(args) + get_setup_complete_hooks(args) + + stages.append({ + # post executing hooks + 'status': 'Wrapping up', + 'fail_msg': 'Failed to complete setup', + 'tasks': [ + { + 'fn': run_post_setup_complete, + 'args': args, + 'fail_msg': 'Failed to complete setup' + } + ] + }) + + return stages + @frappe.whitelist() def setup_complete(args): """Calls hooks for `setup_wizard_complete`, sets home page as `desktop` and clears cache. If wizard breaks, calls `setup_wizard_exception` hook""" + # Setup complete: do not throw an exception, let the user continue to desk if cint(frappe.db.get_single_value('System Settings', 'setup_complete')): - # do not throw an exception if setup is already complete - # let the user continue to desk return - #frappe.throw(_('Setup already complete')) - args = process_args(args) + args = parse_args(args) - try: - if args.language and args.language != "english": - set_default_language(get_language_code(args.lang)) + stages = get_setup_stages(args) - frappe.clear_cache() - - # update system settings - update_system_settings(args) - update_user_name(args) - - for method in frappe.get_hooks("setup_wizard_complete"): - frappe.get_attr(method)(args) + try: + current_task = None + for idx, stage in enumerate(stages): + frappe.publish_realtime('setup_task', {"progress": [idx, len(stages)], + "stage_status": stage.get('status')}, user=frappe.session.user) + + for task in stage.get('tasks'): + current_task = task + task.get('fn')(task.get('args')) + + except Exception: + handle_setup_exception(args) + return {'status': 'fail', 'fail': current_task.get('fail_msg')} + else: + run_setup_success(args) + return {'status': 'ok'} - disable_future_access() +def update_global_settings(args): + if args.language and args.language != "english": + set_default_language(get_language_code(args.lang)) + frappe.clear_cache() - frappe.db.commit() - frappe.clear_cache() - except: - frappe.db.rollback() - if args: - traceback = frappe.get_traceback() - for hook in frappe.get_hooks("setup_wizard_exception"): - frappe.get_attr(hook)(traceback, args) + update_system_settings(args) + update_user_name(args) - raise +def run_post_setup_complete(args): + disable_future_access() + frappe.db.commit() + frappe.clear_cache() - else: - for hook in frappe.get_hooks("setup_wizard_success"): - frappe.get_attr(hook)(args) - install_fixtures.install() +def run_setup_success(args): + for hook in frappe.get_hooks("setup_wizard_success"): + frappe.get_attr(hook)(args) + install_fixtures.install() + +def get_stages_hooks(args): + stages = [] + for method in frappe.get_hooks("setup_wizard_stages"): + stages += frappe.get_attr(method)(args) + return stages + +def get_setup_complete_hooks(args): + stages = [] + for method in frappe.get_hooks("setup_wizard_complete"): + stages.append({ + 'status': 'Executing method', + 'fail_msg': 'Failed to execute method', + 'tasks': [ + { + 'fn': frappe.get_attr(method), + 'args': args, + 'fail_msg': 'Failed to execute method' + } + ] + }) + return stages +def handle_setup_exception(args): + frappe.db.rollback() + if args: + traceback = frappe.get_traceback() + for hook in frappe.get_hooks("setup_wizard_exception"): + frappe.get_attr(hook)(traceback, args) def update_system_settings(args): number_format = get_country_info(args.get("country")).get("number_format", "#,###.##") @@ -126,7 +193,7 @@ def update_user_name(args): if args.get('name'): add_all_roles_to(args.get("name")) -def process_args(args): +def parse_args(args): if not args: args = frappe.local.form_dict if isinstance(args, string_types): @@ -234,14 +301,6 @@ def email_setup_wizard_exception(traceback, args): user_agent = frappe._dict() message = """ -#### Basic Information - -- **Site:** {site} -- **User:** {user} -- **Browser:** {user_agent.platform} {user_agent.browser} version: {user_agent.version} language: {user_agent.language} -- **Browser Languages**: `{accept_languages}` - ---- #### Traceback @@ -257,7 +316,16 @@ def email_setup_wizard_exception(traceback, args): #### Request Headers -
    {headers}
    """.format( +
    {headers}
    + +--- + +#### Basic Information + +- **Site:** {site} +- **User:** {user} +- **Browser:** {user_agent.platform} {user_agent.browser} version: {user_agent.version} language: {user_agent.language} +- **Browser Languages**: `{accept_languages}`""".format( site=frappe.local.site, traceback=traceback, args="\n".join(pretty_args), @@ -268,14 +336,13 @@ def email_setup_wizard_exception(traceback, args): frappe.sendmail(recipients=frappe.local.conf.setup_wizard_exception_email, sender=frappe.session.user, - subject="Exception in Setup Wizard - {}".format(frappe.local.site), + subject="Setup failed: {}".format(frappe.local.site), message=message, delayed=False) def get_language_code(lang): return frappe.db.get_value('Language', {'language_name':lang}) - def enable_twofactor_all_roles(): all_role = frappe.get_doc('Role',{'role_name':'All'}) all_role.two_factor_auth = True diff --git a/frappe/desk/query_builder.py b/frappe/desk/query_builder.py index 0f60bc6fc1..2acf0c4526 100644 --- a/frappe/desk/query_builder.py +++ b/frappe/desk/query_builder.py @@ -10,6 +10,9 @@ from frappe.utils import cint import frappe.defaults from six import text_type +# imports - third-party imports +import pymysql + def get_sql_tables(q): if q.find('WHERE') != -1: tl = q.split('FROM')[1].split('WHERE')[0].split(',') @@ -82,10 +85,9 @@ def guess_type(m): """ Returns fieldtype depending on the MySQLdb Description """ - import MySQLdb - if m in MySQLdb.NUMBER: + if m in pymysql.NUMBER: return 'Currency' - elif m in MySQLdb.DATE: + elif m in pymysql.DATE: return 'Date' else: return 'Data' diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 8b6f32536c..1b7f96b000 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -7,11 +7,13 @@ from __future__ import unicode_literals import frappe, json from six.moves import range import frappe.permissions -import MySQLdb from frappe.model.db_query import DatabaseQuery from frappe import _ from six import text_type, string_types, StringIO +# imports - third-party imports +import pymysql + @frappe.whitelist() def get(): args = get_form_params() @@ -244,7 +246,7 @@ def get_stats(stats, doctype, filters=[]): try: columns = frappe.db.get_table_columns(doctype) - except MySQLdb.OperationalError: + except pymysql.InternalError: # raised when _user_tags column is added on the fly columns = [] @@ -266,7 +268,7 @@ def get_stats(stats, doctype, filters=[]): except frappe.SQLError: # does not work for child tables pass - except MySQLdb.OperationalError: + except pymysql.InternalError: # raised when _user_tags column is added on the fly pass return stats diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index c02f66952b..d1ac3f1bc2 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -2,28 +2,33 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe, json +import frappe from frappe import _ @frappe.whitelist() -def get_all_nodes(tree_method, tree_args, parent): +def get_all_nodes(doctype, parent, tree_method, **filters): '''Recursively gets all data from tree nodes''' + if 'cmd' in filters: + del filters['cmd'] + tree_method = frappe.get_attr(tree_method) if not tree_method in frappe.whitelisted: frappe.throw(_("Not Permitted"), frappe.PermissionError) - frappe.local.form_dict = frappe._dict(json.loads(tree_args)) - frappe.local.form_dict.parent = parent - data = tree_method() + data = tree_method(doctype, parent, **filters) out = [dict(parent=parent, data=data)] + if 'is_root' in filters: + del filters['is_root'] + to_check = [d.value for d in data if d.expandable] + while to_check: - frappe.local.form_dict.parent = to_check.pop() - data = tree_method() - out.append(dict(parent=frappe.local.form_dict.parent, data=data)) + parent = to_check.pop() + data = tree_method(doctype, parent, is_root = False, **filters) + out.append(dict(parent=parent, data=data)) for d in data: if d.expandable: to_check.append(d.value) @@ -31,17 +36,18 @@ def get_all_nodes(tree_method, tree_args, parent): return out @frappe.whitelist() -def get_children(): - doctype = frappe.local.form_dict.get('doctype') +def get_children(doctype, parent='', **filters): parent_field = 'parent_' + doctype.lower().replace(' ', '_') - parent = frappe.form_dict.get("parent") or "" - return frappe.db.sql("""select name as value, + return frappe.db.sql("""select name as value, `{title_field}` as title, is_group as expandable from `tab{ctype}` where docstatus < 2 and ifnull(`{parent_field}`,'') = %s - order by name""".format(ctype=frappe.db.escape(doctype), parent_field=frappe.db.escape(parent_field)), + order by name""".format( + ctype = frappe.db.escape(doctype), + parent_field = frappe.db.escape(parent_field), + title_field = frappe.get_meta(doctype).title_field or 'name'), parent, as_dict=1) @frappe.whitelist() @@ -56,14 +62,14 @@ def add_node(): def make_tree_args(**kwarg): del kwarg['cmd'] - + doctype = kwarg['doctype'] parent_field = 'parent_' + doctype.lower().replace(' ', '_') name_field = kwarg.get('name_field', doctype.lower().replace(' ', '_') + '_name') - + kwarg.update({ name_field: kwarg[name_field], parent_field: kwarg.get("parent") or kwarg.get(parent_field) }) - + return frappe._dict(kwarg) diff --git a/frappe/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe.md b/frappe/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe.md index b3c769828a..24a3283a9f 100755 --- a/frappe/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe.md +++ b/frappe/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe.md @@ -14,4 +14,6 @@ After setting developer mode, clear the cache: $ bench clear-cache +To view the full developer options, you must be logged in as the "Administrator" user. + diff --git a/frappe/docs/user/en/guides/desk/making_charts.md b/frappe/docs/user/en/guides/desk/making_charts.md new file mode 100644 index 0000000000..3d8db9ac16 --- /dev/null +++ b/frappe/docs/user/en/guides/desk/making_charts.md @@ -0,0 +1,3 @@ +# Making Charts + +[**Frappé Charts**](https://frappe.github.io/charts/) enables you to render simple line, bar or percentage graphs for single or multiple discreet sets of data points. You can also set special checkpoint values and summary stats. Check out the docs at https://frappe.github.io/charts/ to learn more. \ No newline at end of file diff --git a/frappe/docs/user/en/guides/desk/making_charts_in_c3js.md b/frappe/docs/user/en/guides/desk/making_charts_in_c3js.md deleted file mode 100644 index c8143590f2..0000000000 --- a/frappe/docs/user/en/guides/desk/making_charts_in_c3js.md +++ /dev/null @@ -1,35 +0,0 @@ -# Making Charts using c3.js - -Frappé bundles the c3.js libary to make charts inside the app and provides a wrapper class so that you can start using charts out of the box. To use chart, you need the x and y data, make a wrapper block and then just make the chart object. - -### Time Series Example - - page.chart = new frappe.ui.Chart({ - // attach the chart here - wrapper: $('
    ').appendTo(page.body), - - // pass the data, like - // ['x', '2016-01-01', '2016-01-02'] - // ['Value', 20, 30] - data: { - x: 'x', - xFormat: '%Y-%m-%d', - columns: [data[0], data[1]], - }, - legend: { - show: false - }, - axis: { - x: { - type: 'timeseries', - }, - y: { - min: 0, - padding: {bottom: 10} - } - } - }); - -### Help - -For more options, see the [c3js.org](http://c3js.org/examples.html) docs \ No newline at end of file diff --git a/frappe/docs/user/en/guides/desk/making_graphs.md b/frappe/docs/user/en/guides/desk/making_graphs.md deleted file mode 100644 index d20f8b88d4..0000000000 --- a/frappe/docs/user/en/guides/desk/making_graphs.md +++ /dev/null @@ -1,100 +0,0 @@ -# Making Graphs - -The Frappé UI **Graph** object enables you to render simple line, bar or percentage graphs for single or multiple discreet sets of data points. You can also set special checkpoint values and summary stats. - -### Example: Line graph -Here's an example of a simple sales graph: - - // Data - let months = ['August, 2016', 'September, 2016', 'October, 2016', 'November, 2016', - 'December, 2016', 'January, 2017', 'February, 2017', 'March, 2017', 'April, 2017', - 'May, 2017', 'June, 2017', 'July, 2017']; - - let values1 = [24100, 31000, 17000, 12000, 27000, 16000, 27400, 11000, 8500, 15000, 4000, 20130]; - let values2 = [17890, 10400, 12350, 20400, 17050, 23000, 7100, 13800, 16000, 20400, 11000, 13000]; - let goal = 25000; - let current_val = 20130; - - let g = new frappe.ui.Graph({ - parent: $('.form-graph').empty(), - height: 200, // optional - mode: 'line', // 'line', 'bar' or 'percentage' - - title: 'Sales', - subtitle: 'Monthly', - - y: [ - { - title: 'Data 1', - values: values1, - formatted: values1.map(d => '$ ' + d), - color: 'green' // Indicator colors: 'grey', 'blue', 'red', - // 'green', 'light-green', 'orange', 'purple', 'darkgrey', - // 'black', 'yellow', 'lightblue' - }, - { - title: 'Data 2', - values: values2, - formatted: values2.map(d => '$ ' + d), - color: 'light-green' - } - ], - - x: { - values: months.map(d => d.substring(0, 3)), - formatted: months - }, - - specific_values: [ - { - name: 'Goal', - line_type: 'dashed', // 'dashed' or 'solid' - value: goal - }, - ], - - summary: [ - { - name: 'This month', - color: 'orange', - value: '$ ' + current_val - }, - { - name: 'Goal', - color: 'blue', - value: '$ ' + goal - }, - { - name: 'Completed', - color: 'green', - value: (current_val/goal*100).toFixed(1) + "%" - } - ] - }); - - - -`bar` mode yeilds: - - - -You can set the `colors` property of `x` to an array of color values for `percentage` mode: - - - -You can also change the values of an existing graph with a new set of `y` values: - - setTimeout(() => { - g.change_values([ - { - values: data[2], - formatted: data[2].map(d => d + 'L') - }, - { - values: data[3], - formatted: data[3].map(d => d + 'L') - } - ]); - }, 1000); - - diff --git a/frappe/docs/user/en/guides/integration/google_gsuite.md b/frappe/docs/user/en/guides/integration/google_gsuite.md index dd8e86f90f..d44773c271 100644 --- a/frappe/docs/user/en/guides/integration/google_gsuite.md +++ b/frappe/docs/user/en/guides/integration/google_gsuite.md @@ -1,7 +1,10 @@ # Google GSuite -You can create and attach Google GSuite Docs to your Documents using your predefined GSuite Templates. -These Templates could use variables from Doctype that will be automatically filled. +Frappe allows you to use Google's Gsuite documents as templates, generate from them a new Gsuite document that will be placed in a chosen folder. Variables can populated in both the body and the name of the Gsuite document using the standard Jinja2 format. Once generated, the Gsuite document will remain associate to the DocType as an attachment. + +The Gsuite document is generated by invoking the "attach file" function of any DocType. + +A common use cases of this features is populating contracts from customer/employee/supplier data. ## 1. Enable integration with Google Gsuite @@ -21,52 +24,51 @@ These Templates could use variables from Doctype that will be automatically fill ### 1.2 Get Google access -1. Go to your Google project console and select your project or create a new. [https://console.developers.google.com](https://console.developers.google.com) +1. Go to your Google project console and select your project or create a new one. [https://console.developers.google.com](https://console.developers.google.com) 1. In Library click on **Google Drive API** and **Enable** 1. Click on **Credentials > Create Credentials > OAuth Client ID** 1. Fill the form with: - Web Application - - Authorized redirect URI as **http://{{ yoursite }}/?cmd=frappe.integrations.doctype.gsuite_settings.gsuite_settings.gsuite_callback** + - Authorized redirect URI as `http://{{ yoursite }}/?cmd=frappe.integrations.doctype.gsuite_settings.gsuite_settings.gsuite_callback` 1. Copy the Client ID and Client Secret into **Desk > Explore > Integrations > GSuite Settings > Client ID and Client Secret** 1. Save GSuite Settings ### 1.3 Test Script -1. Click on **Allow GSuite Access** and you will be redirected to select the user and give access. If you have any error please verify you are using the correct Authorized redirect URI. +1. Click on **Allow GSuite Access** and you will be redirected to select the user and give access. If you have any error please verify you are using the correct Authorized Redirect URI. + You can find the complete URI Gsuite redirected to in the final part of the URL of the error page. Check that the protocol `http://` or `https://` matches the one your using. 1. Click on **Run Script test**. You should be asked to give permission. ## 2. GSuite Templates ### 2.1 Google Document as Template -1. Create a new Document or use one you already have. Set variables as you need. Variables are defined with ***{{VARIABLE}}*** with ***VARIABLE*** is the field of your Doctype +1. Create a new Document or use one you already have. Set variables as you need. Variables are defined with `{{VARIABLE}}` with ***VARIABLE*** is the field of your Doctype For Example, - If this document will be used to employee and the Doctype has the field ***name*** then you can use it in Google Docs ad {{name}} + If this document will be used to employee and the Doctype has the field ***name*** then you can use it in Google Docs ad `{{name}}` -1. Get the ID of that Document from url of your document - - For example: in this address the ID is in bold - https://docs.google.com/document/d/**1Y2_btbwSqPIILLcJstHnSm1u5dgYE0QJspcZBImZQso**/edit +1. Get the ID of that Document from url of your document. + For example: in this document url `https://docs.google.com/document/d/1Y2_btbwSqPIILLcJstHnSm1u5dgYE0QJspcZBImZQso/edit` the document ID is `1Y2_btbwSqPIILLcJstHnSm1u5dgYE0QJspcZBImZQso` 1. Get the ID of the folder where you want to place the generated documents. (You can step this point if you want to place the generated documents in Google Drive root. ) - For example: in this folder url the ID is in bold - https://drive.google.com/drive/u/0/folders/**0BxmFzZZUHbgyQzVJNzY5eG5jbmc** + For example: in this folder url `https://drive.google.com/drive/u/0/folders/0BxmFzZZUHbgyQzVJNzY5eG5jbmc` the folder ID is `0BxmFzZZUHbgyQzVJNzY5eG5jbmc` ### 2.2 Associate the Template to a Doctype 1. Go to **Desk > Explore > Integrations > GSuite Templates > New** -1. Fill the form with: - - Template Name (Example: Employee Contract) - - Related DocType (Example: Employee) - - Template ID is the Document ID you get from your Google Docs (Example: 1Y2_btbwSqPIILLcJstHnSm1u5dgYE0QJspcZBImZQso) - - Document name is the name of the new files. You can use field from DocType (Example: Employee Contract of {name}) - - Destination ID is the folder ID of your files created from this Template. (Example: 0BxmFzZZUHbgyQzVJNzY5eG5jbmc) +2. Fill the form with: + - Template Name (Example: `Employee Contract`) + - Related DocType (Example: `Employee`) + - Template ID is the Document ID you get from your Google Docs (Example: `1Y2_btbwSqPIILLcJstHnSm1u5dgYE0QJspcZBImZQso`) + - Document name is the name of the new files. You can use field from DocType (Example: `Employee Contract of {name}`) + - Destination ID is the folder ID of your files created from this Template. (Example: `0BxmFzZZUHbgyQzVJNzY5eG5jbmc`) ## 3. Create Documents -1. Go to a Document you already have a Template (Example: Employee > John Doe) +1. Go to a Document you already have a Template for (Example: Employee > John Doe) 2. Click on **Attach File** -3. O **GSuite Document** section select the Template and click **Attach** +3. On **GSuite Document** section select the Template and click **Attach** 4. You should see the generated document is already created and attached +5. Clicking on the attached document will open it inside Gsuite diff --git a/frappe/docs/user/en/guides/integration/rest_api.md b/frappe/docs/user/en/guides/integration/rest_api.md index 58e3815364..041545addd 100755 --- a/frappe/docs/user/en/guides/integration/rest_api.md +++ b/frappe/docs/user/en/guides/integration/rest_api.md @@ -210,6 +210,8 @@ _Response:_ "parentfield": null } } + +Note: `POST` requests are to be sent along with `X-Frappe-CSRF-Token:` header. #### Read diff --git a/frappe/docs/user/en/tutorial/task-runner.md b/frappe/docs/user/en/tutorial/task-runner.md index 0afdfeaa1b..a700f1cafd 100755 --- a/frappe/docs/user/en/tutorial/task-runner.md +++ b/frappe/docs/user/en/tutorial/task-runner.md @@ -1,8 +1,8 @@ # Scheduled Tasks -Finally, an application also has to send email notifications and do other kind of scheduled tasks. In Frappé, if you have setup the bench, the task / scheduler is setup via Celery using Redis Queue. +Finally, an application also has to send email notifications and do other kind of scheduled tasks. In Frappé, if you have setup the bench, the task / scheduler is setup via RQ using Redis Queue. -To add a new task handler, go to `hooks.py` and add a new handler. Default handlers are `all`, `daily`, `weekly`, `monthly`. The `all` handler is called every 3 minutes by default. +To add a new task handler, go to `hooks.py` and add a new handler. Default handlers are `all`, `daily`, `weekly`, `monthly`, `cron`. The `all` handler is called every 4 minutes by default. # Scheduled Tasks # --------------- @@ -11,6 +11,15 @@ To add a new task handler, go to `hooks.py` and add a new handler. Default handl "daily": [ "library_management.tasks.daily" ], + "cron": { + "0/10 * * * *": [ + "library_management.task.run_every_ten_mins" + ], + "15 18 * * *": [ + "library_management.task.every_day_at_18_15" + ] + } + } Here we can point to a Python function and that function will be executed every day. Let us look what this function looks like: @@ -21,6 +30,14 @@ Here we can point to a Python function and that function will be executed every from __future__ import unicode_literals import frappe from frappe.utils import datediff, nowdate, format_date, add_days + + def every_ten_minutes(): + # stuff to do every 10 minutes + pass + + def every_day_at_18_15(): + # stuff to do every day at 6:15pm + pass def daily(): loan_period = frappe.db.get_value("Library Management Settings", diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 8f468f3de5..01b8566982 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -139,7 +139,9 @@ class AutoEmailReport(Document): recipients = self.email_to.split(), subject = self.name, message = message, - attachments = attachments + attachments = attachments, + reference_doctype = self.doctype, + reference_name = self.name ) @frappe.whitelist() diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 53cb6b8dd7..1350d132df 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -457,8 +457,8 @@ class EmailAccount(Document): # try and match by subject and sender # if sent by same sender with same subject, # append it to old coversation - subject = frappe.as_unicode(strip(re.sub("(^\s*(Fw|FW|fwd)[^:]*:|\s*(Re|RE)[^:]*:\s*)*", - "", email.subject))) + subject = frappe.as_unicode(strip(re.sub("(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*", + "", email.subject, 0, flags=re.IGNORECASE))) parent = frappe.db.get_all(self.append_to, filters={ self.sender_field: email.from_email, @@ -597,7 +597,7 @@ class EmailAccount(Document): flags = frappe.db.sql("""select name, communication, uid, action from `tabEmail Flag Queue` where is_completed=0 and email_account='{email_account}' - """.format(email_account=self.name), as_dict=True) + """.format(email_account=frappe.db.escape(self.name)), as_dict=True) uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags } if flags and uid_list: diff --git a/frappe/email/doctype/email_alert/email_alert.py b/frappe/email/doctype/email_alert/email_alert.py index 10a964d88f..a90dbb53da 100755 --- a/frappe/email/doctype/email_alert/email_alert.py +++ b/frappe/email/doctype/email_alert/email_alert.py @@ -13,6 +13,10 @@ from frappe.modules.utils import export_module_json, get_doc_module from markdown2 import markdown from six import string_types +# imports - third-party imports +import pymysql +from pymysql.constants import ER + class EmailAlert(Document): def onload(self): '''load message''' @@ -117,7 +121,8 @@ def get_context(context): please enable Allow Print For {0} in Print Settings""".format(status)), title=_("Error in Email Alert")) else: - return [frappe.attach_print(doc.doctype, doc.name, None, self.print_format)] + return [{"print_format_attachment":1, "doctype":doc.doctype, "name": doc.name, + "print_format":self.print_format}] context = get_context(doc) recipients = [] @@ -237,8 +242,8 @@ def evaluate_alert(doc, alert, event): if event=="Value Change" and not doc.is_new(): try: db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) - except frappe.DatabaseOperationalError as e: - if e.args[0]==1054: + except pymysql.InternalError as e: + if e.args[0]== ER.BAD_FIELD_ERROR: alert.db_set('enabled', 0) frappe.log_error('Email Alert {0} has been disabled due to missing field'.format(alert.name)) return diff --git a/frappe/email/doctype/email_alert/test_email_alert.py b/frappe/email/doctype/email_alert/test_email_alert.py index 302f40a717..53bfe777a1 100755 --- a/frappe/email/doctype/email_alert/test_email_alert.py +++ b/frappe/email/doctype/email_alert/test_email_alert.py @@ -132,6 +132,7 @@ class TestEmailAlert(unittest.TestCase): self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", "reference_name": event.name, "status":"Not Sent"})) + frappe.set_user('Administrator') frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True) # not today, so no alert diff --git a/frappe/email/doctype/standard_reply/standard_reply.json b/frappe/email/doctype/standard_reply/standard_reply.json index b27f681270..4765e3f3ad 100644 --- a/frappe/email/doctype/standard_reply/standard_reply.json +++ b/frappe/email/doctype/standard_reply/standard_reply.json @@ -1,8 +1,9 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, - "autoname": "field:subject", + "autoname": "Prompt", "beta": 0, "creation": "2014-06-19 05:20:26.331041", "custom": 0, @@ -13,6 +14,7 @@ "engine": "InnoDB", "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -23,6 +25,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, "label": "Subject", @@ -40,6 +43,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -50,6 +54,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, "label": "Response", @@ -67,6 +72,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -78,6 +84,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Owner", @@ -96,6 +103,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -106,6 +114,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "length": 0, @@ -123,6 +132,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -133,6 +143,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Standard Reply Help", @@ -152,18 +163,18 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-comment", "idx": 0, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-12-29 14:40:02.827994", + "modified": "2017-11-10 08:25:07.708599", "modified_by": "Administrator", "module": "Email", "name": "Standard Reply", @@ -180,7 +191,6 @@ "export": 0, "if_owner": 0, "import": 0, - "is_custom": 0, "permlevel": 0, "print": 0, "read": 1, @@ -201,7 +211,6 @@ "export": 1, "if_owner": 0, "import": 1, - "is_custom": 0, "permlevel": 0, "print": 0, "read": 1, @@ -216,6 +225,7 @@ "quick_entry": 0, "read_only": 0, "read_only_onload": 0, + "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1, diff --git a/frappe/email/doctype/standard_reply/test_standard_reply.js b/frappe/email/doctype/standard_reply/test_standard_reply.js new file mode 100644 index 0000000000..03e601fdd3 --- /dev/null +++ b/frappe/email/doctype/standard_reply/test_standard_reply.js @@ -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: Standard Reply", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Standard Reply + () => frappe.tests.make('Standard Reply', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index c12f2a857e..8ab7ae5c85 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -15,7 +15,7 @@ from email.header import Header def get_email(recipients, sender='', msg='', subject='[No Subject]', text_content = None, footer=None, print_html=None, formatted=None, attachments=None, - content=None, reply_to=None, cc=[], email_account=None, expose_recipients=None, + content=None, reply_to=None, cc=[], bcc=[], email_account=None, expose_recipients=None, inline_images=[], header=None): """ Prepare an email with the following format: - multipart/mixed @@ -27,7 +27,7 @@ def get_email(recipients, sender='', msg='', subject='[No Subject]', - attachment """ content = content or msg - emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, email_account=email_account, expose_recipients=expose_recipients) + emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, bcc=bcc, email_account=email_account, expose_recipients=expose_recipients) if not content.strip().startswith("<"): content = markdown(content) @@ -51,7 +51,7 @@ class EMail: Also provides a clean way to add binary `FileData` attachments Also sets all messages as multipart/alternative for cleaner reading in text-only clients """ - def __init__(self, sender='', recipients=(), subject='', alternative=0, reply_to=None, cc=(), email_account=None, expose_recipients=None): + def __init__(self, sender='', recipients=(), subject='', alternative=0, reply_to=None, cc=(), bcc=(), email_account=None, expose_recipients=None): from email import charset as Charset Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8') @@ -72,6 +72,7 @@ class EMail: self.msg_alternative = MIMEMultipart('alternative') self.msg_root.attach(self.msg_alternative) self.cc = cc or [] + self.bcc = bcc or [] self.html_set = False self.email_account = email_account or get_outgoing_email_account(sender=sender) @@ -176,8 +177,9 @@ class EMail: self.recipients = [strip(r) for r in self.recipients] self.cc = [strip(r) for r in self.cc] + self.bcc = [strip(r) for r in self.bcc] - for e in self.recipients + (self.cc or []): + for e in self.recipients + (self.cc or []) + (self.bcc or []): validate_email_add(e, True) def replace_sender(self): @@ -207,6 +209,7 @@ class EMail: "To": ', '.join(self.recipients) if self.expose_recipients=="header" else "", "Date": email.utils.formatdate(), "Reply-To": self.reply_to if self.reply_to else None, + "Bcc": ', '.join(self.bcc) if self.bcc else None, "CC": ', '.join(self.cc) if self.cc and self.expose_recipients=="header" else None, 'X-Frappe-Site': get_url(), } diff --git a/frappe/email/queue.py b/frappe/email/queue.py index d34592f055..085dfb0c18 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -21,7 +21,7 @@ class EmailLimitCrossedError(frappe.ValidationError): pass def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, - attachments=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None, + attachments=None, reply_to=None, cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, header=None): @@ -61,6 +61,9 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if isinstance(cc, string_types): cc = split_emails(cc) + if isinstance(bcc, string_types): + bcc = split_emails(bcc) + if isinstance(send_after, int): send_after = add_days(nowdate(), send_after) @@ -112,6 +115,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= attachments=attachments, reply_to=reply_to, cc=cc, + bcc=bcc, message_id=message_id, in_reply_to=in_reply_to, send_after=send_after, @@ -158,11 +162,13 @@ def get_email_queue(recipients, sender, subject, **kwargs): e.priority = kwargs.get('send_priority') attachments = kwargs.get('attachments') if attachments: - # store attachments with fid, to be attached on-demand later + # store attachments with fid or print format details, to be attached on-demand later _attachments = [] for att in attachments: if att.get('fid'): _attachments.append(att) + elif att.get("print_format_attachment") == 1: + _attachments.append(att) e.attachments = json.dumps(_attachments) try: @@ -174,6 +180,7 @@ def get_email_queue(recipients, sender, subject, **kwargs): attachments=kwargs.get('attachments'), reply_to=kwargs.get('reply_to'), cc=kwargs.get('cc'), + bcc=kwargs.get('bcc'), email_account=kwargs.get('email_account'), expose_recipients=kwargs.get('expose_recipients'), inline_images=kwargs.get('inline_images'), @@ -194,7 +201,7 @@ def get_email_queue(recipients, sender, subject, **kwargs): frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}'.format(mail.sender, ', '.join(mail.recipients)), 'Email Not Sent') - e.set_recipients(recipients + kwargs.get('cc', [])) + e.set_recipients(recipients + kwargs.get('cc', []) + kwargs.get('bcc', [])) e.reference_doctype = kwargs.get('reference_doctype') e.reference_name = kwargs.get('reference_name') e.add_unsubscribe_link = kwargs.get("add_unsubscribe_link") @@ -204,6 +211,7 @@ def get_email_queue(recipients, sender, subject, **kwargs): e.communication = kwargs.get('communication') e.send_after = kwargs.get('send_after') e.show_as_cc = ",".join(kwargs.get('cc', [])) + e.show_as_bcc = ",".join(kwargs.get('bcc', [])) e.insert(ignore_permissions=True) return e @@ -511,17 +519,22 @@ def prepare_message(email, recipient, recipients_list): for attachment in attachments: if attachment.get('fcontent'): continue - fid = attachment.get('fid') - if not fid: continue - - fname, fcontent = get_file(fid) - attachment.update({ - 'fname': fname, - 'fcontent': fcontent, - 'parent': msg_obj - }) - attachment.pop("fid", None) - add_attachment(**attachment) + fid = attachment.get("fid") + if fid: + fname, fcontent = get_file(fid) + attachment.update({ + 'fname': fname, + 'fcontent': fcontent, + 'parent': msg_obj + }) + attachment.pop("fid", None) + add_attachment(**attachment) + + elif attachment.get("print_format_attachment") == 1: + attachment.pop("print_format_attachment", None) + print_format_file = frappe.attach_print(**attachment) + print_format_file.update({"parent": msg_obj}) + add_attachment(**print_format_file) return msg_obj.as_string() diff --git a/frappe/exceptions.py b/frappe/exceptions.py index ae9fca7e7a..34fbd33d02 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -4,11 +4,11 @@ from __future__ import unicode_literals # BEWARE don't put anything in this file except exceptions - from werkzeug.exceptions import NotFound -from MySQLdb import ProgrammingError as SQLError, Error -from MySQLdb import OperationalError as DatabaseOperationalError +# imports - third-party imports +from pymysql import ProgrammingError as SQLError, Error +# from pymysql import OperationalError as DatabaseOperationalError class ValidationError(Exception): http_status_code = 417 @@ -46,7 +46,6 @@ class Redirect(Exception): class CSRFTokenError(Exception): http_status_code = 400 - class ImproperDBConfigurationError(Error): """ Used when frappe detects that database or tables are not properly @@ -58,7 +57,6 @@ class ImproperDBConfigurationError(Error): super(ImproperDBConfigurationError, self).__init__(msg) self.reason = reason - class DuplicateEntryError(NameError):pass class DataError(ValidationError): pass class UnknownDomainError(Exception): pass diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 53a14ffc5c..f3a251f72d 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -66,6 +66,8 @@ "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", "number_format": "#,###.##", "timezones": [ "America/Anguilla" @@ -92,6 +94,8 @@ "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", "number_format": "#,###.##", "timezones": [ "America/Antigua" @@ -721,6 +725,8 @@ "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", "number_format": "#,###.##", "timezones": [ "America/Dominica" @@ -985,6 +991,8 @@ "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", "number_format": "#,###.##", "timezones": [ "America/Grenada" @@ -1670,6 +1678,8 @@ "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", "number_format": "#,###.##", "timezones": [ "America/Montserrat" @@ -2043,6 +2053,8 @@ "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", "number_format": "#,###.##", "timezones": [ "America/St_Kitts" @@ -2053,6 +2065,8 @@ "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", "number_format": "#,###.##", "timezones": [ "America/St_Lucia" @@ -2071,6 +2085,8 @@ "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", "number_format": "#,###.##", "timezones": [ "America/St_Vincent" diff --git a/frappe/hooks.py b/frappe/hooks.py index 503e60f811..11ab305296 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -68,7 +68,7 @@ calendars = ["Event"] # login on_session_creation = [ - "frappe.core.doctype.communication.feed.login_feed", + "frappe.core.doctype.activity_log.feed.login_feed", "frappe.core.doctype.user.user.notify_admin_access_to_system_manager", "frappe.limits.check_if_expired", "frappe.utils.scheduler.reset_enabled_scheduler_events", @@ -110,7 +110,7 @@ doc_events = { "*": { "on_update": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.core.doctype.communication.feed.update_feed" + "frappe.core.doctype.activity_log.feed.update_feed" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ @@ -153,7 +153,7 @@ scheduler_events = { "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", "frappe.core.doctype.feedback_request.feedback_request.delete_feedback_request", - "frappe.core.doctype.authentication_log.authentication_log.clear_authentication_logs" + "frappe.core.doctype.activity_log.activity_log.clear_authentication_logs" ], "daily_long": [ "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index f8daf965b5..35027b90c9 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -13,7 +13,7 @@ from frappe.utils.background_jobs import enqueue from six.moves.urllib.parse import urlparse, parse_qs from frappe.integrations.utils import make_post_request from frappe.utils import (cint, split_emails, get_request_site_address, cstr, - get_files_path, get_backups_path, encode, get_url) + get_files_path, get_backups_path, get_url, encode) ignore_list = [".DS_Store"] @@ -145,27 +145,37 @@ def upload_from_folder(path, dropbox_folder, dropbox_client, did_not_upload, err error_log.append(frappe.get_traceback()) def upload_file_to_dropbox(filename, folder, dropbox_client): + """upload files with chunk of 15 mb to reduce session append calls""" + create_folder_if_not_exists(folder, dropbox_client) - chunk_size = 4 * 1024 * 1024 + chunk_size = 15 * 1024 * 1024 file_size = os.path.getsize(encode(filename)) mode = (dropbox.files.WriteMode.overwrite) f = open(encode(filename), 'rb') path = "{0}/{1}".format(folder, os.path.basename(filename)) - if file_size <= chunk_size: - dropbox_client.files_upload(f.read(), path, mode) - else: - upload_session_start_result = dropbox_client.files_upload_session_start(f.read(chunk_size)) - cursor = dropbox.files.UploadSessionCursor(session_id=upload_session_start_result.session_id, offset=f.tell()) - commit = dropbox.files.CommitInfo(path=path, mode=mode) - - while f.tell() < file_size: - if ((file_size - f.tell()) <= chunk_size): - dropbox_client.files_upload_session_finish(f.read(chunk_size), cursor, commit) - else: - dropbox_client.files_upload_session_append(f.read(chunk_size), cursor.session_id,cursor.offset) - cursor.offset = f.tell() + try: + if file_size <= chunk_size: + dropbox_client.files_upload(f.read(), path, mode) + else: + upload_session_start_result = dropbox_client.files_upload_session_start(f.read(chunk_size)) + cursor = dropbox.files.UploadSessionCursor(session_id=upload_session_start_result.session_id, offset=f.tell()) + commit = dropbox.files.CommitInfo(path=path, mode=mode) + + while f.tell() < file_size: + if ((file_size - f.tell()) <= chunk_size): + dropbox_client.files_upload_session_finish(f.read(chunk_size), cursor, commit) + else: + dropbox_client.files_upload_session_append(f.read(chunk_size), cursor.session_id,cursor.offset) + cursor.offset = f.tell() + except dropbox.exceptions.ApiError as e: + if isinstance(e.error, dropbox.files.UploadError): + error = "File Path: {path}\n".foramt(path=path) + error += frappe.get_traceback() + frappe.log_error(error) + else: + raise def create_folder_if_not_exists(folder, dropbox_client): try: @@ -211,7 +221,7 @@ def get_redirect_url(): if response.get("message"): return response["message"] - except Exception as e: + except Exception: frappe.log_error() frappe.throw( _("Something went wrong while generating dropbox access token. Please check error log for more details.") diff --git a/frappe/integrations/doctype/google_maps/__init__.py b/frappe/integrations/doctype/google_maps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/google_maps/google_maps.js b/frappe/integrations/doctype/google_maps/google_maps.js new file mode 100644 index 0000000000..2205b9c05b --- /dev/null +++ b/frappe/integrations/doctype/google_maps/google_maps.js @@ -0,0 +1,5 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Google Maps', { +}); diff --git a/frappe/integrations/doctype/google_maps/google_maps.json b/frappe/integrations/doctype/google_maps/google_maps.json new file mode 100644 index 0000000000..4b9a3f6411 --- /dev/null +++ b/frappe/integrations/doctype/google_maps/google_maps.json @@ -0,0 +1,152 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-10-16 17:13:05.684227", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "enabled", + "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": "Enabled", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "client_key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Client Key", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "home_address", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Home Address", + "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 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2017-10-16 17:13:05.684227", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Google Maps", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/google_maps/google_maps.py b/frappe/integrations/doctype/google_maps/google_maps.py new file mode 100644 index 0000000000..d7b278e4c2 --- /dev/null +++ b/frappe/integrations/doctype/google_maps/google_maps.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document +import datetime + +class GoogleMaps(Document): + def validate(self): + if self.enabled: + if not self.client_key: + frappe.throw(_("Client key is required")) + if not self.home_address: + frappe.throw(_("Home Address is required")) + +def round_timedelta(td, period): + """Round timedelta""" + period_seconds = period.total_seconds() + half_period_seconds = period_seconds / 2 + remainder = td.total_seconds() % period_seconds + if remainder >= half_period_seconds: + return datetime.timedelta(seconds=td.total_seconds() + (period_seconds - remainder)) + else: + return datetime.timedelta(seconds=td.total_seconds() - remainder) + +def format_address(address): + """Customer Address format """ + address = frappe.get_doc('Address', address) + return '{}, {}, {}, {}'.format(address.address_line1, address.city, address.pincode, address.country) diff --git a/frappe/integrations/doctype/google_maps/test_google_maps.js b/frappe/integrations/doctype/google_maps/test_google_maps.js new file mode 100644 index 0000000000..ff6a8d88a5 --- /dev/null +++ b/frappe/integrations/doctype/google_maps/test_google_maps.js @@ -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: Google Maps", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Google Maps + () => frappe.tests.make('Google Maps', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/integrations/doctype/google_maps/test_google_maps.py b/frappe/integrations/doctype/google_maps/test_google_maps.py new file mode 100644 index 0000000000..8fe158186d --- /dev/null +++ b/frappe/integrations/doctype/google_maps/test_google_maps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals +import unittest + +class TestGoogleMaps(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/gsuite_settings/gsuite_settings.json b/frappe/integrations/doctype/gsuite_settings/gsuite_settings.json index 49cf853853..65ed904c74 100644 --- a/frappe/integrations/doctype/gsuite_settings/gsuite_settings.json +++ b/frappe/integrations/doctype/gsuite_settings/gsuite_settings.json @@ -250,7 +250,7 @@ "label": "Script Code", "length": 0, "no_copy": 0, - "options": "// ERPNEXT GSuite integration\n//\n\nfunction doGet(e){\n return ContentService.createTextOutput('ok');\n}\n\nfunction doPost(e) {\n var p = JSON.parse(e.postData.contents);\n\n switch(p.exec){\n case 'new':\n var url = createDoc(p);\n result = { 'url': url };\n break;\n case 'test':\n result = { 'test':'ping' , 'version':'1.0'}\n }\n return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);\n}\n\nfunction replaceVars(body,p){\n for (key in p) {\n if (p.hasOwnProperty(key)) {\n if (p[key] != null) {\n body.replaceText('{{'+key+'}}', p[key]);\n }\n }\n } \n}\n\nfunction createDoc(p) {\n if(p.destination){\n var folder = DriveApp.getFolderById(p.destination);\n } else {\n var folder = DriveApp.getRootFolder();\n }\n var template = DriveApp.getFileById( p.template )\n var newfile = template.makeCopy( p.filename , folder );\n\n switch(newfile.getMimeType()){\n case MimeType.GOOGLE_DOCS:\n var body = DocumentApp.openById(newfile.getId()).getBody();\n replaceVars(body,p.vars);\n break;\n case MimeType.GOOGLE_SHEETS:\n //TBD\n case MimeType.GOOGLE_SLIDES:\n //TBD\n }\n return newfile.getUrl()\n}\n\n", + "options": "
    // ERPNEXT GSuite integration\n//\n\nfunction doGet(e){\n  return ContentService.createTextOutput('ok');\n}\n\nfunction doPost(e) {\n  var p = JSON.parse(e.postData.contents);\n\n  switch(p.exec){\n    case 'new':\n      var url = createDoc(p);\n      result = { 'url': url };\n      break;\n    case 'test':\n      result = { 'test':'ping' , 'version':'1.0'}\n  }\n  return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);\n}\n\nfunction replaceVars(body,p){\n  for (key in p) {\n    if (p.hasOwnProperty(key)) {\n      if (p[key] != null) {\n        body.replaceText('{{'+key+'}}', p[key]);\n      }\n    }\n  }    \n}\n\nfunction createDoc(p) {\n  if(p.destination){\n    var folder = DriveApp.getFolderById(p.destination);\n  } else {\n    var folder = DriveApp.getRootFolder();\n  }\n  var template = DriveApp.getFileById( p.template )\n  var newfile = template.makeCopy( p.filename , folder );\n\n  switch(newfile.getMimeType()){\n    case MimeType.GOOGLE_DOCS:\n      var body = DocumentApp.openById(newfile.getId()).getBody();\n      replaceVars(body,p.vars);\n      break;\n    case MimeType.GOOGLE_SHEETS:\n      //TBD\n    case MimeType.GOOGLE_SLIDES:\n      //TBD\n  }\n  return newfile.getUrl()\n}\n\n
    ", "permlevel": 0, "precision": "", "print_hide": 0, @@ -365,7 +365,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-05-19 15:28:44.663715", + "modified": "2017-10-20 16:11:47.757030", "modified_by": "Administrator", "module": "Integrations", "name": "GSuite Settings", diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.py b/frappe/integrations/doctype/paypal_settings/paypal_settings.py index 36eff70fa1..eb28b999dd 100644 --- a/frappe/integrations/doctype/paypal_settings/paypal_settings.py +++ b/frappe/integrations/doctype/paypal_settings/paypal_settings.py @@ -227,11 +227,14 @@ def confirm_payment(token): }, "Completed") if data.get("reference_doctype") and data.get("reference_docname"): - redirect_url = frappe.get_doc(data.get("reference_doctype"), data.get("reference_docname")).run_method("on_payment_authorized", "Completed") + custom_redirect_to = frappe.get_doc(data.get("reference_doctype"), + data.get("reference_docname")).run_method("on_payment_authorized", "Completed") frappe.db.commit() - if not redirect_url: - redirect_url = '/integrations/payment-success' + if custom_redirect_to: + redirect_to = custom_redirect_to + + redirect_url = '/integrations/payment-success' else: redirect_url = "/integrations/payment-failed" diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py index a7da7c48b1..4d6c5d5f60 100644 --- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py @@ -108,10 +108,7 @@ class RazorpaySettings(Document): until it is explicitly captured by merchant. """ data = json.loads(self.integration_request.data) - settings = self.get_settings(data) - redirect_to = data.get('notes', {}).get('redirect_to') or None - redirect_message = data.get('notes', {}).get('redirect_message') or None try: resp = make_get_request("https://api.razorpay.com/v1/payments/{0}" @@ -119,7 +116,7 @@ class RazorpaySettings(Document): settings.api_secret)) if resp.get("status") == "authorized": - self.integration_request.db_set('status', 'Authorized', update_modified=False) + self.integration_request.update_status(data, 'Authorized') self.flags.status_changed_to = "Authorized" else: @@ -132,6 +129,9 @@ class RazorpaySettings(Document): status = frappe.flags.integration_request.status_code + redirect_to = data.get('notes', {}).get('redirect_to') or None + redirect_message = data.get('notes', {}).get('redirect_message') or None + if self.flags.status_changed_to == "Authorized": if self.data.reference_doctype and self.data.reference_docname: custom_redirect_to = None diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index c266f419ee..a8125e213d 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from six import iteritems, string_types +import datetime import frappe, sys from frappe import _ from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta, @@ -181,7 +182,7 @@ class BaseDocument(object): return value - def get_valid_dict(self, sanitize=True): + def get_valid_dict(self, sanitize=True, convert_dates_to_str=False): d = frappe._dict() for fieldname in self.meta.get_valid_columns(): d[fieldname] = self.get(fieldname) @@ -215,6 +216,9 @@ class BaseDocument(object): if isinstance(d[fieldname], list) and df.fieldtype != 'Table': frappe.throw(_('Value for {0} cannot be a list').format(_(df.label))) + if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time)): + d[fieldname] = str(d[fieldname]) + return d def init_valid_columns(self): @@ -244,8 +248,8 @@ class BaseDocument(object): def is_new(self): return self.get("__islocal") - def as_dict(self, no_nulls=False, no_default_fields=False): - doc = self.get_valid_dict() + def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False): + doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) doc["doctype"] = self.doctype for df in self.meta.get_table_fields(): children = self.get(df.fieldname) or [] @@ -316,7 +320,6 @@ class BaseDocument(object): raise else: raise - self.set("__islocal", False) def db_update(self): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 0cd71684c9..4b067fd60b 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -308,15 +308,7 @@ class DatabaseQuery(object): if f.operator.lower() == 'between' and \ (f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))): - from_date = None - to_date = None - if f.value and isinstance(f.value, (list, tuple)): - if len(f.value) >= 1: from_date = f.value[0] - if len(f.value) >= 2: to_date = f.value[1] - - value = "'%s' AND '%s'" % ( - add_to_date(get_datetime(from_date),days=-1).strftime("%Y-%m-%d %H:%M:%S.%f"), - get_datetime(to_date).strftime("%Y-%m-%d %H:%M:%S.%f")) + value = get_between_date_filter(f.value) fallback = "'0000-00-00 00:00:00'" elif df and df.fieldtype=="Date": @@ -571,3 +563,73 @@ def get_order_by(doctype, meta): order_by = "`tab{0}`.docstatus asc, {1}".format(doctype, order_by) return order_by + + +@frappe.whitelist() +def get_list(doctype, *args, **kwargs): + '''wrapper for DatabaseQuery''' + kwargs.pop('cmd', None) + return DatabaseQuery(doctype).execute(None, *args, **kwargs) + +@frappe.whitelist() +def get_count(doctype, filters=None): + if filters: + filters = json.loads(filters) + + if is_parent_only_filter(doctype, filters): + if isinstance(filters, list): + filters = frappe.utils.make_filter_dict(filters) + + return frappe.db.count(doctype, filters=filters) + + else: + # If filters contain child table as well as parent doctype - Join + tables, conditions = ['`tab{0}`'.format(doctype)], [] + for f in filters: + fieldname = '`tab{0}`.{1}'.format(f[0], f[1]) + table = '`tab{0}`'.format(f[0]) + + if table not in tables: + tables.append(table) + + conditions.append('{fieldname} {operator} "{value}"'.format(fieldname=fieldname, + operator=f[2], value=f[3])) + + if doctype != f[0]: + join_condition = '`tab{child_doctype}`.parent =`tab{doctype}`.name'.format(child_doctype=f[0], doctype=doctype) + if join_condition not in conditions: + conditions.append(join_condition) + + return frappe.db.sql_list("""select count(*) from {0} + where {1}""".format(','.join(tables), ' and '.join(conditions)), debug=0) + +def is_parent_only_filter(doctype, filters): + #check if filters contains only parent doctype + only_parent_doctype = True + + if isinstance(filters, list): + for flt in filters: + if doctype not in flt: + only_parent_doctype = False + if 'Between' in flt: + flt[3] = get_between_date_filter(flt[3]) + + return only_parent_doctype + +def get_between_date_filter(value): + ''' + return the formattted date as per the given example + [u'2017-11-01', u'2017-11-03'] => '2017-11-01 00:00:00.000000' AND '2017-11-04 00:00:00.000000' + ''' + from_date = None + to_date = None + + if value and isinstance(value, (list, tuple)): + if len(value) >= 1: from_date = value[0] + if len(value) >= 2: to_date = value[1] + + data = "'%s' AND '%s'" % ( + get_datetime(from_date).strftime("%Y-%m-%d %H:%M:%S.%f"), + add_to_date(get_datetime(to_date),days=1).strftime("%Y-%m-%d %H:%M:%S.%f")) + + return data diff --git a/frappe/model/db_schema.py b/frappe/model/db_schema.py index fb3de07332..64d7daca71 100644 --- a/frappe/model/db_schema.py +++ b/frappe/model/db_schema.py @@ -13,7 +13,10 @@ import os import frappe from frappe import _ from frappe.utils import cstr, cint, flt -import MySQLdb + +# imports - third-party imports +import pymysql +from pymysql.constants import ER class InvalidColumnName(frappe.ValidationError): pass @@ -26,25 +29,26 @@ type_map = { ,'Float': ('decimal', '18,6') ,'Percent': ('decimal', '18,6') ,'Check': ('int', '1') - ,'Small Text': ('text', '') - ,'Long Text': ('longtext', '') + ,'Small Text': ('text', '') + ,'Long Text': ('longtext', '') ,'Code': ('longtext', '') - ,'Text Editor': ('longtext', '') + ,'Text Editor': ('longtext', '') ,'Date': ('date', '') - ,'Datetime': ('datetime', '6') + ,'Datetime': ('datetime', '6') ,'Time': ('time', '6') ,'Text': ('text', '') ,'Data': ('varchar', varchar_len) ,'Link': ('varchar', varchar_len) - ,'Dynamic Link':('varchar', varchar_len) - ,'Password': ('varchar', varchar_len) + ,'Dynamic Link': ('varchar', varchar_len) + ,'Password': ('varchar', varchar_len) ,'Select': ('varchar', varchar_len) - ,'Read Only': ('varchar', varchar_len) + ,'Read Only': ('varchar', varchar_len) ,'Attach': ('text', '') - ,'Attach Image':('text', '') - ,'Signature': ('longtext', '') + ,'Attach Image': ('text', '') + ,'Signature': ('longtext', '') ,'Color': ('varchar', varchar_len) ,'Barcode': ('longtext', '') + ,'Geolocation': ('longtext', '') } default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner', @@ -120,8 +124,8 @@ class DbTable: max_length = frappe.db.sql("""select max(char_length(`{fieldname}`)) from `tab{doctype}`"""\ .format(fieldname=col.fieldname, doctype=self.doctype)) - except MySQLdb.OperationalError as e: - if e.args[0]==1054: + except pymysql.InternalError as e: + if e.args[0] == ER.BAD_FIELD_ERROR: # Unknown column 'column_name' in 'field list' continue diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 6ff425657c..6d55bbb79a 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -189,7 +189,7 @@ def check_if_doc_is_linked(doc, method="Delete"): for item in frappe.db.get_values(link_dt, {link_field:doc.name}, ["name", "parent", "parenttype", "docstatus"], as_dict=True): linked_doctype = item.parenttype if item.parent else link_dt - if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version'): + if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log"): # don't check for communication and todo! continue @@ -207,7 +207,7 @@ def check_if_doc_is_linked(doc, method="Delete"): def check_if_doc_is_dynamically_linked(doc, method="Delete"): '''Raise `frappe.LinkExistsError` if the document is dynamically linked''' for df in get_dynamic_link_map().get(doc.doctype, []): - if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version'): + if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version'): # don't check for communication and todo! continue @@ -292,6 +292,18 @@ def delete_dynamic_links(doctype, name): set timeline_doctype=null, timeline_name=null where timeline_doctype=%s and timeline_name=%s""", (doctype, name)) + # unlink activity_log reference_doctype + frappe.db.sql("""update `tabActivity Log` + set reference_doctype=null, reference_name=null + where + reference_doctype=%s + and reference_name=%s""", (doctype, name)) + + # unlink activity_log timeline_doctype + frappe.db.sql("""update `tabActivity Log` + set timeline_doctype=null, timeline_name=null + where timeline_doctype=%s and timeline_name=%s""", (doctype, name)) + def insert_feed(doc): from frappe.utils import get_fullname diff --git a/frappe/model/document.py b/frappe/model/document.py index 029ac1b969..50f2170412 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -357,7 +357,11 @@ class Document(BaseDocument): def get_doc_before_save(self): if not getattr(self, '_doc_before_save', None): - self._doc_before_save = frappe.get_doc(self.doctype, self.name) + try: + self._doc_before_save = frappe.get_doc(self.doctype, self.name) + except frappe.DoesNotExistError: + self._doc_before_save = None + frappe.clear_last_message() return self._doc_before_save def set_new_name(self, force=False): @@ -435,7 +439,6 @@ class Document(BaseDocument): def _validate(self): self._validate_mandatory() self._validate_selects() - self._validate_constants() self._validate_length() self._extract_images_from_text_editor() self._sanitize_content() @@ -444,17 +447,67 @@ class Document(BaseDocument): children = self.get_all_children() for d in children: d._validate_selects() - d._validate_constants() d._validate_length() d._extract_images_from_text_editor() d._sanitize_content() d._save_passwords() + self.validate_set_only_once() + if self.is_new(): # don't set fields like _assign, _comments for new doc for fieldname in optional_fields: self.set(fieldname, None) + def validate_set_only_once(self): + '''Validate that fields are not changed if not in insert''' + set_only_once_fields = self.meta.get_set_only_once_fields() + + if set_only_once_fields and self._doc_before_save: + # document exists before saving + for field in set_only_once_fields: + fail = False + value = self.get(field.fieldname) + original_value = self._doc_before_save.get(field.fieldname) + + if field.fieldtype=='Table': + fail = not self.is_child_table_same(field.fieldname) + elif field.fieldtype in ('Date', 'Datetime', 'Time'): + fail = str(value) != str(original_value) + else: + fail = value != original_value + + if fail: + frappe.throw(_("Value cannot be changed for {0}").format(self.meta.get_label(field.fieldname)), + frappe.CannotChangeConstantError) + + return False + + def is_child_table_same(self, fieldname): + '''Validate child table is same as original table before saving''' + value = self.get(fieldname) + original_value = self._doc_before_save.get(fieldname) + same = True + + if len(original_value) != len(value): + same = False + else: + # check all child entries + for i, d in enumerate(original_value): + new_child = value[i].as_dict(convert_dates_to_str = True) + original_child = d.as_dict(convert_dates_to_str = True) + + # all fields must be same other than modified and modified_by + for key in ('modified', 'modified_by', 'creation'): + del new_child[key] + del original_child[key] + + if original_child != new_child: + same = False + break + + return same + def apply_fieldlevel_read_permissions(self): '''Remove values the user is not allowed to read (called when loading in desk)''' has_higher_permlevel = False @@ -796,10 +849,8 @@ class Document(BaseDocument): Will also update title_field if set""" + self.load_doc_before_save() self.reset_seen() - self._doc_before_save = None - if not self.is_new() and getattr(self.meta, 'track_changes', False): - self.get_doc_before_save() if self.flags.ignore_validate: return @@ -817,6 +868,14 @@ class Document(BaseDocument): self.set_title_field() + def load_doc_before_save(self): + '''Save load document from db before saving''' + self._doc_before_save = None + if not (self.is_new() + and (getattr(self.meta, 'track_changes', False) + or self.meta.get_set_only_once_fields())): + self.get_doc_before_save() + def run_post_save_methods(self): """Run standard methods after `INSERT` or `UPDATE`. Standard Methods are: diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 81f5708dc8..6b9654c1f4 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -96,6 +96,12 @@ class Meta(Document): def get_image_fields(self): return self.get("fields", {"fieldtype": "Attach Image"}) + def get_set_only_once_fields(self): + '''Return fields with `set_only_once` set''' + if not hasattr(self, "_set_only_once_fields"): + self._set_only_once_fields = self.get("fields", {"set_only_once": 1}) + return self._set_only_once_fields + def get_table_fields(self): if not hasattr(self, "_table_fields"): if self.name!="DocType": diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py index ade3614c8e..16d9882d6e 100644 --- a/frappe/modules/import_file.py +++ b/frappe/modules/import_file.py @@ -95,10 +95,10 @@ ignore_doctypes = [""] def import_doc(docdict, force=False, data_import=False, pre_process=None, ignore_version=None, reset_permissions=False): - frappe.flags.in_import = True docdict["__islocal"] = 1 doc = frappe.get_doc(docdict) + doc.flags.ignore_version = ignore_version if pre_process: pre_process(doc) @@ -128,5 +128,7 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None, doc.flags.ignore_validate = True doc.flags.ignore_permissions = True doc.flags.ignore_mandatory = True + doc.insert() + frappe.flags.in_import = False diff --git a/frappe/patches.txt b/frappe/patches.txt index a127ab73ca..49bc7bb978 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -3,27 +3,28 @@ frappe.patches.v5_0.convert_to_barracuda_and_utf8mb4 execute:frappe.utils.global_search.setup_global_search_table() frappe.patches.v8_0.update_global_search_table frappe.patches.v7_0.update_auth -frappe.patches.v7_1.rename_scheduler_log_to_error_log -frappe.patches.v6_1.rename_file_data -frappe.patches.v7_0.re_route #2016-06-27 -frappe.patches.v7_2.remove_in_filter frappe.patches.v8_0.drop_in_dialog #2017-09-22 +frappe.patches.v7_2.remove_in_filter execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2017-03-03 execute:frappe.reload_doc('core', 'doctype', 'docperm') #2017-03-03 execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22 +execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01 +execute:frappe.reload_doc('core', 'doctype', 'activity_log') +frappe.patches.v7_1.rename_scheduler_log_to_error_log +frappe.patches.v6_1.rename_file_data +frappe.patches.v7_0.re_route #2016-06-27 frappe.patches.v8_0.drop_is_custom_from_docperm frappe.patches.v8_0.update_records_in_global_search #11-05-2017 frappe.patches.v8_0.update_published_in_global_search execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'deleted_document') execute:frappe.reload_doc('core', 'doctype', 'domain_settings') -execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01 frappe.patches.v8_0.rename_page_role_to_has_role #2017-03-16 frappe.patches.v7_2.setup_custom_perms #2017-01-19 frappe.patches.v8_0.set_user_permission_for_page_and_report #2017-03-20 execute:frappe.reload_doc('core', 'doctype', 'role') #2017-05-23 -execute:frappe.reload_doc('core', 'doctype', 'user') +execute:frappe.reload_doc('core', 'doctype', 'user') #2017-10-27 execute:frappe.reload_doc('custom', 'doctype', 'custom_field') #2015-10-19 execute:frappe.reload_doc('core', 'doctype', 'page') #2013-13-26 execute:frappe.reload_doc('core', 'doctype', 'report') #2014-06-03 @@ -194,7 +195,9 @@ frappe.patches.v8_5.delete_email_group_member_with_invalid_emails frappe.patches.v8_x.update_user_permission frappe.patches.v8_5.patch_event_colors frappe.patches.v8_10.delete_static_web_page_from_global_search -frappe.patches.v8_x.add_bgn_xaf_xof_currencies frappe.patches.v9_1.add_sms_sender_name_as_parameters frappe.patches.v9_1.resave_domain_settings -frappe.patches.v9_1.revert_domain_settings \ No newline at end of file +frappe.patches.v9_1.revert_domain_settings +frappe.patches.v9_1.move_feed_to_activity_log +execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True) +frappe.patches.v10_0.reload_countries_and_currencies \ No newline at end of file diff --git a/frappe/patches/v10_0/__init__.py b/frappe/patches/v10_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v10_0/reload_countries_and_currencies.py b/frappe/patches/v10_0/reload_countries_and_currencies.py new file mode 100644 index 0000000000..927fb10a7c --- /dev/null +++ b/frappe/patches/v10_0/reload_countries_and_currencies.py @@ -0,0 +1,8 @@ +""" +Run this after updating country_info.json and or +""" +from frappe.utils.install import import_country_and_currency + + +def execute(): + import_country_and_currency() diff --git a/frappe/patches/v8_x/add_bgn_xaf_xof_currencies.py b/frappe/patches/v8_x/add_bgn_xaf_xof_currencies.py deleted file mode 100644 index 298f385f40..0000000000 --- a/frappe/patches/v8_x/add_bgn_xaf_xof_currencies.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -This will add the following currencies: -1. BGN (Bulgarian Lev) to Bulgaria. -2. XAF (Central African CFA Franc) to Cameroon, Republic of Congo, Chad, Gabon, Equitorial Guinea and - Central African Republic. -3. XOF (West African CFA Franc) to Benin, Niger, Burkina Faso, Mali, Senegal, Togo, Ivory Coast and Guinea Bissau. -""" -from frappe.utils.install import import_country_and_currency - - -def execute(): - import_country_and_currency() diff --git a/frappe/patches/v9_1/move_feed_to_activity_log.py b/frappe/patches/v9_1/move_feed_to_activity_log.py new file mode 100644 index 0000000000..2d35e1e908 --- /dev/null +++ b/frappe/patches/v9_1/move_feed_to_activity_log.py @@ -0,0 +1,23 @@ +import frappe +from frappe.utils.background_jobs import enqueue + +def execute(): + comm_records_count = frappe.db.count("Communication", {"comment_type": "Updated"}) + if comm_records_count > 100000: + enqueue(method=move_data_from_communication_to_activity_log, queue='short', now=True) + else: + move_data_from_communication_to_activity_log() + +def move_data_from_communication_to_activity_log(): + frappe.reload_doc("core", "doctype", "communication") + frappe.reload_doc("core", "doctype", "activity_log") + + frappe.db.sql("""insert into `tabActivity Log` (name, owner, modified, creation, status, communication_date, + reference_doctype, reference_name, timeline_doctype, timeline_name, link_doctype, link_name, subject, content, user) + select name, owner, modified, creation, status, communication_date, + reference_doctype, reference_name, timeline_doctype, timeline_name, link_doctype, link_name, subject, content, user + from `tabCommunication` + where comment_type = 'Updated'""") + + frappe.db.sql("""delete from `tabCommunication` where comment_type = 'Updated'""") + frappe.delete_doc("DocType", "Authentication Log") \ No newline at end of file diff --git a/frappe/public/build.json b/frappe/public/build.json index 1a418248dd..ae0a9cbd3c 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -53,7 +53,10 @@ "public/js/frappe/form/controls/html.js", "public/js/frappe/form/controls/heading.js", "public/js/frappe/form/controls/autocomplete.js", - "public/js/frappe/form/controls/barcode.js" + "public/js/frappe/form/controls/barcode.js", + "public/js/frappe/form/controls/geolocation.js", + "public/js/frappe/form/controls/multiselect.js", + "public/js/frappe/form/controls/multicheck.js" ], "js/dialog.min.js": [ "public/js/frappe/dom.js", @@ -94,12 +97,17 @@ "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" + "public/js/frappe/form/controls/heading.js", + "public/js/frappe/form/controls/geolocation.js" ], "css/desk.min.css": [ "public/js/lib/datepicker/datepicker.min.css", "public/js/lib/awesomplete/awesomplete.css", "public/js/lib/summernote/summernote.css", + "public/js/lib/leaflet/leaflet.css", + "public/js/lib/leaflet/leaflet.draw.css", + "public/js/lib/leaflet/L.Control.Locate.css", + "public/js/lib/leaflet/easy-button.css", "public/css/bootstrap.css", "public/css/font-awesome.css", "public/css/octicons/octicons.css", @@ -114,7 +122,8 @@ "public/css/form.css", "public/css/mobile.css", "public/css/kanban.css", - "public/css/charts.css" + "public/css/controls.css", + "public/css/tags.css" ], "css/frappe-rtl.css": [ "public/css/bootstrap-rtl.css", @@ -124,7 +133,6 @@ "js/libs.min.js": [ "public/js/lib/awesomplete/awesomplete.min.js", "public/js/lib/Sortable.min.js", - "public/js/lib/taggle/taggle.min.js", "public/js/lib/jquery/jquery.hotkeys.js", "public/js/lib/summernote/summernote.js", "public/js/lib/bootstrap.min.js", @@ -136,8 +144,12 @@ "public/js/frappe/translate.js", "public/js/lib/datepicker/datepicker.min.js", "public/js/lib/datepicker/locale-all.js", - "public/js/lib/jquery.jrumble.min.js", - "public/js/lib/webcam.min.js" + "public/js/lib/frappe-charts/frappe-charts.min.iife.js", + "public/js/lib/webcam.min.js", + "public/js/lib/leaflet/leaflet.js", + "public/js/lib/leaflet/leaflet.draw.js", + "public/js/lib/leaflet/L.Control.Locate.js", + "public/js/lib/leaflet/easy-button.js" ], "js/desk.min.js": [ "public/js/frappe/class.js", @@ -157,7 +169,6 @@ "public/js/frappe/socketio_client.js", "public/js/frappe/router.js", "public/js/frappe/defaults.js", - "public/js/frappe/checkbox_editor.js", "public/js/frappe/roles_editor.js", "public/js/lib/microtemplate.js", @@ -230,7 +241,6 @@ "public/js/frappe/desk.js", "public/js/frappe/query_string.js", - "public/js/frappe/ui/charts.js", "public/js/frappe/ui/comment.js", "public/js/frappe/misc/rating_icons.html", @@ -254,7 +264,7 @@ "public/js/frappe/form/templates/form_links.html", "public/js/frappe/views/formview.js", "public/js/legacy/form.js", - "public/js/legacy/clientscriptAPI.js", + "public/js/legacy/client_script_helpers.js", "public/js/frappe/form/toolbar.js", "public/js/frappe/form/dashboard.js", "public/js/frappe/form/document_flow.js", @@ -281,7 +291,6 @@ "public/js/frappe/form/quick_entry.js" ], "css/list.min.css": [ - "public/js/lib/taggle/taggle.min.css", "public/css/list.css", "public/css/calendar.css", "public/css/role_editor.css", @@ -296,6 +305,7 @@ "public/js/frappe/ui/filters/filters.js", "public/js/frappe/ui/filters/edit_filter.html", "public/js/frappe/ui/tags.js", + "public/js/frappe/ui/tag_editor.js", "public/js/frappe/ui/like.js", "public/js/frappe/ui/liked_by.html", "public/html/print_template.html", diff --git a/frappe/public/css/charts.css b/frappe/public/css/charts.css deleted file mode 100644 index f5d279568a..0000000000 --- a/frappe/public/css/charts.css +++ /dev/null @@ -1,284 +0,0 @@ -/* charts */ -.chart-container .graph-focus-margin { - margin: 0px 5%; -} -.chart-container .graphics { - margin-top: 10px; - padding-top: 10px; - padding-bottom: 10px; - position: relative; -} -.chart-container .graph-stats-group { - display: flex; - justify-content: space-around; - flex: 1; -} -.chart-container .graph-stats-container { - display: flex; - justify-content: space-around; - padding-top: 10px; -} -.chart-container .graph-stats-container .stats { - padding-bottom: 15px; -} -.chart-container .graph-stats-container .stats-title { - color: #8D99A6; -} -.chart-container .graph-stats-container .stats-value { - font-size: 20px; - font-weight: 300; -} -.chart-container .graph-stats-container .stats-description { - font-size: 12px; - color: #8D99A6; -} -.chart-container .graph-stats-container .graph-data .stats-value { - color: #98d85b; -} -.chart-container .axis, -.chart-container .chart-label { - font-size: 10px; - fill: #555b51; -} -.chart-container .axis line, -.chart-container .chart-label line { - stroke: rgba(27, 31, 35, 0.2); -} -.chart-container .percentage-graph .progress { - margin-bottom: 0px; -} -.chart-container .data-points circle { - stroke: #fff; - stroke-width: 2; -} -.chart-container .data-points path { - fill: none; - stroke-opacity: 1; - stroke-width: 2px; -} -.chart-container line.dashed { - stroke-dasharray: 5,3; -} -.chart-container .tick.x-axis-label { - display: block; -} -.chart-container .tick .specific-value { - text-anchor: start; -} -.chart-container .tick .y-value-text { - text-anchor: end; -} -.chart-container .tick .x-value-text { - text-anchor: middle; -} -.graph-svg-tip { - position: absolute; - z-index: 99999; - padding: 10px; - font-size: 12px; - color: #959da5; - text-align: center; - background: rgba(0, 0, 0, 0.8); - border-radius: 3px; -} -.graph-svg-tip.comparison { - padding: 0; - text-align: left; - pointer-events: none; -} -.graph-svg-tip.comparison .title { - display: block; - padding: 10px; - margin: 0; - font-weight: 600; - line-height: 1; - pointer-events: none; -} -.graph-svg-tip.comparison ul { - margin: 0; - white-space: nowrap; - list-style: none; -} -.graph-svg-tip.comparison li { - display: inline-block; - padding: 5px 10px; -} -.graph-svg-tip ul, -.graph-svg-tip ol { - padding-left: 0; - display: flex; -} -.graph-svg-tip ul.data-point-list li { - min-width: 90px; - flex: 1; -} -.graph-svg-tip strong { - color: #dfe2e5; -} -.graph-svg-tip .svg-pointer { - position: absolute; - bottom: -10px; - left: 50%; - width: 5px; - height: 5px; - margin: 0 0 0 -5px; - content: " "; - border: 5px solid transparent; - border-top-color: rgba(0, 0, 0, 0.8); -} -.stroke.grey { - stroke: #F0F4F7; -} -.stroke.blue { - stroke: #5e64ff; -} -.stroke.red { - stroke: #ff5858; -} -.stroke.light-green { - stroke: #98d85b; -} -.stroke.lightgreen { - stroke: #98d85b; -} -.stroke.green { - stroke: #28a745; -} -.stroke.orange { - stroke: #ffa00a; -} -.stroke.purple { - stroke: #743ee2; -} -.stroke.darkgrey { - stroke: #b8c2cc; -} -.stroke.black { - stroke: #36414C; -} -.stroke.yellow { - stroke: #FEEF72; -} -.stroke.light-blue { - stroke: #7CD6FD; -} -.stroke.lightblue { - stroke: #7CD6FD; -} -.fill.grey { - fill: #F0F4F7; -} -.fill.blue { - fill: #5e64ff; -} -.fill.red { - fill: #ff5858; -} -.fill.light-green { - fill: #98d85b; -} -.fill.lightgreen { - fill: #98d85b; -} -.fill.green { - fill: #28a745; -} -.fill.orange { - fill: #ffa00a; -} -.fill.purple { - fill: #743ee2; -} -.fill.darkgrey { - fill: #b8c2cc; -} -.fill.black { - fill: #36414C; -} -.fill.yellow { - fill: #FEEF72; -} -.fill.light-blue { - fill: #7CD6FD; -} -.fill.lightblue { - fill: #7CD6FD; -} -.background.grey { - background: #F0F4F7; -} -.background.blue { - background: #5e64ff; -} -.background.red { - background: #ff5858; -} -.background.light-green { - background: #98d85b; -} -.background.lightgreen { - background: #98d85b; -} -.background.green { - background: #28a745; -} -.background.orange { - background: #ffa00a; -} -.background.purple { - background: #743ee2; -} -.background.darkgrey { - background: #b8c2cc; -} -.background.black { - background: #36414C; -} -.background.yellow { - background: #FEEF72; -} -.background.light-blue { - background: #7CD6FD; -} -.background.lightblue { - background: #7CD6FD; -} -.border-top.grey { - border-top: 3px solid #F0F4F7; -} -.border-top.blue { - border-top: 3px solid #5e64ff; -} -.border-top.red { - border-top: 3px solid #ff5858; -} -.border-top.light-green { - border-top: 3px solid #98d85b; -} -.border-top.lightgreen { - border-top: 3px solid #98d85b; -} -.border-top.green { - border-top: 3px solid #28a745; -} -.border-top.orange { - border-top: 3px solid #ffa00a; -} -.border-top.purple { - border-top: 3px solid #743ee2; -} -.border-top.darkgrey { - border-top: 3px solid #b8c2cc; -} -.border-top.black { - border-top: 3px solid #36414C; -} -.border-top.yellow { - border-top: 3px solid #FEEF72; -} -.border-top.light-blue { - border-top: 3px solid #7CD6FD; -} -.border-top.lightblue { - border-top: 3px solid #7CD6FD; -} diff --git a/frappe/public/css/controls.css b/frappe/public/css/controls.css new file mode 100644 index 0000000000..ab97425429 --- /dev/null +++ b/frappe/public/css/controls.css @@ -0,0 +1,12 @@ +.unit-checkbox { + color: #36414c; + margin-top: 5px; + margin-bottom: 5px; +} +.unit-checkbox + .checkbox { + margin-top: 5px; + margin-bottom: 5px; +} +.frappe-control .select-all { + margin-right: 5px; +} diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 76b38ba164..0bd6a0b53b 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -475,6 +475,11 @@ fieldset[disabled] .form-control { .modal-title { margin-top: 5px; } +.btn-primary.disabled { + background-color: #b1bdca; + color: #fff; + border-color: #b1bdca; +} .form-control { position: relative; } @@ -965,50 +970,9 @@ li.user-progress .progress-bar { .note-editor.note-frame .note-editing-area .note-editable { color: #36414C; } -.c3 svg { - font-family: inherit; - font-size: 10px; - color: #36414C; -} -.c3-line { - stroke-width: 3px; -} -.c3-tooltip { - box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.1); - border-radius: 3px; - opacity: 1; -} -.c3-tooltip tr { - border: none; -} -.c3-tooltip th { - color: #8D99A6; - background-color: #ffffff; - font-size: 12px; - font-weight: normal; - padding: 2px 5px; - text-align: left; - border: 1px solid #d1d8dd; -} -.c3-tooltip td { - color: #36414C; - font-size: 11px; - padding: 3px 6px; - background-color: #fff; - border: 1px solid #d1d8dd; -} -.c3-tooltip td > span { - display: inline-block; - width: 10px; - height: 10px; - margin-right: 6px; -} -.c3-tooltip td.value { - text-align: right; -} input[type="checkbox"] { - visibility: hidden; position: relative; + height: 16px; } input[type="checkbox"]:before { position: absolute; @@ -1026,12 +990,21 @@ input[type="checkbox"]:before { -webkit-transition: 150ms color; -o-transition: 150ms color; transition: 150ms color; + background-color: white; + padding: 1px; + margin: -1px; +} +input[type="checkbox"]:focus:before { + color: #8D99A6; } input[type="checkbox"]:checked:before { content: '\f14a'; font-size: 13px; color: #3b99fc; } +input[type="checkbox"]:focus { + outline: none; +} .multiselect-empty-state { min-height: 300px; display: flex; diff --git a/frappe/public/css/desktop.css b/frappe/public/css/desktop.css index 8b918ca8fe..4d91482772 100644 --- a/frappe/public/css/desktop.css +++ b/frappe/public/css/desktop.css @@ -62,6 +62,44 @@ body[data-route="desktop"] .navbar-default { transition: 0.2s; -webkit-transition: 0.2s; } +@-webkit-keyframes wiggle { + 0% { + -webkit-transform: rotate(3deg); + } + 50% { + -webkit-transform: rotate(-3deg); + } + 100% { + -webkit-transform: rotate(3deg); + } +} +@-moz-keyframes wiggle { + 0% { + -moz-transform: rotate(3deg); + } + 50% { + -moz-transform: rotate(-3deg); + } + 100% { + -moz-transform: rotate(3deg); + } +} +@keyframes wiggle { + 0% { + transform: rotate(3deg); + } + 50% { + transform: rotate(-3deg); + } + 100% { + transform: rotate(3deg); + } +} +.wiggle { + -webkit-animation: wiggle 0.2s linear infinite; + -moz-animation: wiggle 0.2s linear infinite; + animation: wiggle 0.2s linear infinite; +} .circle { position: absolute; right: 20px; diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index abed4bc105..b3e8862e2a 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -34,11 +34,14 @@ } .set-filters .btn-group { margin-right: 10px; + white-space: nowrap; + font-size: 0; } .set-filters .btn-group .btn-default { background-color: transparent; border: 1px solid #d1d8dd; color: #8D99A6; + float: none; } .filter-box { border-bottom: 1px solid #d1d8dd; @@ -405,6 +408,22 @@ .pswp__bg { background-color: #fff !important; } +.pswp__more-items { + position: absolute; + bottom: 12px; + left: 50%; + transform: translateX(-50%); +} +.pswp__more-item { + display: inline-block; + margin: 5px; + height: 100px; + cursor: pointer; + border: 1px solid #d1d8dd; +} +.pswp__more-item img { + max-height: 100%; +} .gantt .details-container .heading { margin-bottom: 10px; font-size: 12px; diff --git a/frappe/public/css/page.css b/frappe/public/css/page.css index 3fae923d04..9dd2c5b866 100644 --- a/frappe/public/css/page.css +++ b/frappe/public/css/page.css @@ -99,6 +99,8 @@ margin: 0px; padding-right: 15px; padding-top: 10px; + display: flex; + flex-wrap: wrap; border-bottom: 1px solid #d1d8dd; background-color: #F7FAFC; } @@ -277,6 +279,9 @@ select.input-sm { opacity: 1; cursor: pointer; } +.setup-wizard-slide .progress-bar { + background-color: #5e64ff; +} .page-card-container { padding: 70px; } @@ -310,49 +315,3 @@ select.input-sm { justify-content: center; align-items: center; } -@keyframes lds-rolling { - 0% { - -webkit-transform: translate(-50%, -50%) rotate(0deg); - transform: translate(-50%, -50%) rotate(0deg); - } - 100% { - -webkit-transform: translate(-50%, -50%) rotate(360deg); - transform: translate(-50%, -50%) rotate(360deg); - } -} -@-webkit-keyframes lds-rolling { - 0% { - -webkit-transform: translate(-50%, -50%) rotate(0deg); - transform: translate(-50%, -50%) rotate(0deg); - } - 100% { - -webkit-transform: translate(-50%, -50%) rotate(360deg); - transform: translate(-50%, -50%) rotate(360deg); - } -} -.lds-rolling { - -webkit-transform: translate(-100px, -100px) scale(1) translate(100px, 100px); - transform: translate(-100px, -100px) scale(1) translate(100px, 100px); -} -.lds-rolling div { - position: absolute; - width: 60px; - height: 60px; - border: 3px solid #d1d8dd; - border-top-color: transparent; - border-radius: 50%; - -webkit-animation: lds-rolling 1s linear infinite; - animation: lds-rolling 1s linear infinite; - top: 50px; - left: 50px; -} -.lds-rolling div:after { - position: absolute; - width: 60px; - height: 60px; - border: 3px solid #d1d8dd; - border-top-color: transparent; - border-radius: 50%; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} diff --git a/frappe/public/css/tags.css b/frappe/public/css/tags.css new file mode 100644 index 0000000000..fb77c61e81 --- /dev/null +++ b/frappe/public/css/tags.css @@ -0,0 +1,19 @@ +.tags-list { + float: left; + width: 100%; + padding-left: 3px; +} +.tags-input { + width: 100px; + font-size: 11px; + border: none; + outline: none; +} +.tags-list-item { + display: inline-block; + margin: 0px 3px; +} +.tags-placeholder { + display: inline-block; + font-size: 11px; +} diff --git a/frappe/public/images/leaflet/layers-2x.png b/frappe/public/images/leaflet/layers-2x.png new file mode 100644 index 0000000000..200c333dca Binary files /dev/null and b/frappe/public/images/leaflet/layers-2x.png differ diff --git a/frappe/public/images/leaflet/layers.png b/frappe/public/images/leaflet/layers.png new file mode 100644 index 0000000000..1a72e5784b Binary files /dev/null and b/frappe/public/images/leaflet/layers.png differ diff --git a/frappe/public/images/leaflet/leafletmarker-icon.png b/frappe/public/images/leaflet/leafletmarker-icon.png new file mode 100644 index 0000000000..950edf2467 Binary files /dev/null and b/frappe/public/images/leaflet/leafletmarker-icon.png differ diff --git a/frappe/public/images/leaflet/leafletmarker-shadow.png b/frappe/public/images/leaflet/leafletmarker-shadow.png new file mode 100644 index 0000000000..9fd2979532 Binary files /dev/null and b/frappe/public/images/leaflet/leafletmarker-shadow.png differ diff --git a/frappe/public/images/leaflet/lego.png b/frappe/public/images/leaflet/lego.png new file mode 100644 index 0000000000..f173107335 Binary files /dev/null and b/frappe/public/images/leaflet/lego.png differ diff --git a/frappe/public/images/leaflet/marker-icon-2x.png b/frappe/public/images/leaflet/marker-icon-2x.png new file mode 100644 index 0000000000..e4abba3b51 Binary files /dev/null and b/frappe/public/images/leaflet/marker-icon-2x.png differ diff --git a/frappe/public/images/leaflet/marker-icon.png b/frappe/public/images/leaflet/marker-icon.png new file mode 100644 index 0000000000..950edf2467 Binary files /dev/null and b/frappe/public/images/leaflet/marker-icon.png differ diff --git a/frappe/public/images/leaflet/marker-shadow.png b/frappe/public/images/leaflet/marker-shadow.png new file mode 100644 index 0000000000..9fd2979532 Binary files /dev/null and b/frappe/public/images/leaflet/marker-shadow.png differ diff --git a/frappe/public/images/leaflet/spritesheet-2x.png b/frappe/public/images/leaflet/spritesheet-2x.png new file mode 100644 index 0000000000..c45231aff8 Binary files /dev/null and b/frappe/public/images/leaflet/spritesheet-2x.png differ diff --git a/frappe/public/images/leaflet/spritesheet.png b/frappe/public/images/leaflet/spritesheet.png new file mode 100644 index 0000000000..97d71c6805 Binary files /dev/null and b/frappe/public/images/leaflet/spritesheet.png differ diff --git a/frappe/public/images/leaflet/spritesheet.svg b/frappe/public/images/leaflet/spritesheet.svg new file mode 100644 index 0000000000..3c00f30314 --- /dev/null +++ b/frappe/public/images/leaflet/spritesheet.svg @@ -0,0 +1,156 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frappe/public/js/frappe/checkbox_editor.js b/frappe/public/js/frappe/checkbox_editor.js deleted file mode 100644 index 45891cb882..0000000000 --- a/frappe/public/js/frappe/checkbox_editor.js +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -// opts: -// frm -// wrapper -// get_items -// add_btn_label -// remove_btn_label -// field_mapper: -// cdt -// child_table_field -// item_field -// attribute - -frappe.CheckboxEditor = Class.extend({ - init: function(opts) { - $.extend(this, opts); - - this.doctype = this.field_mapper.cdt; - this.fieldname = this.field_mapper.child_table_field; - this.item_fieldname = this.field_mapper.item_field; - - $(this.wrapper).html('
    ' + __("Loading") + '...
    '); - - if(this.get_items) { - this.get_items(); - } - }, - render_items: function(callback) { - let me = this; - $(this.wrapper).empty(); - - if(this.checkbox_selector) { - let toolbar = $('

    \ -

    ').appendTo($(this.wrapper)); - - toolbar.find(".btn-add") - .html(__(this.add_btn_label)) - .on("click", function() { - $(me.wrapper).find('input[type="checkbox"]').each(function(i, check) { - if(!$(check).is(":checked")) { - check.checked = true; - } - }); - }); - - toolbar.find(".btn-remove") - .html(__(this.remove_btn_label)) - .on("click", function() { - $(me.wrapper).find('input[type="checkbox"]').each(function(i, check) { - if($(check).is(":checked")) { - check.checked = false; - } - }); - }); - } - - $.each(this.items, function(i, item) { - $(me.wrapper).append(frappe.render(me.editor_template, {'item': item})); - }); - - $(this.wrapper).find('input[type="checkbox"]').change(function() { - if(me.fieldname && me.doctype && me.item_field) { - me.set_items_in_table(); - me.frm.dirty(); - } - }); - - callback && callback() - }, - show: function() { - let me = this; - - // uncheck all items - $(this.wrapper).find('input[type="checkbox"]') - .each(function(i, checkbox) { checkbox.checked = false; }); - - // set user items as checked - $.each((me.frm.doc[this.fieldname] || []), function(i, row) { - let selector = repl('[%(attribute)s="%(value)s"] input[type="checkbox"]', { - attribute: me.attribute, - value: row[me.item_fieldname] - }); - - let checkbox = $(me.wrapper) - .find(selector).get(0); - if(checkbox) checkbox.checked = true; - }); - }, - - get_selected_unselected_items: function() { - let checked_items = []; - let unchecked_items = []; - let selector = repl('[%(attribute)s]', { attribute: this.attribute }); - let me = this; - - $(this.wrapper).find(selector).each(function() { - if($(this).find('input[type="checkbox"]:checked').length) { - checked_items.push($(this).attr(me.attribute)); - } else { - unchecked_items.push($(this).attr(me.attribute)); - } - }); - - return { - checked_items: checked_items, - unchecked_items: unchecked_items - } - }, - - set_items_in_table: function() { - let opts = this.get_selected_unselected_items(); - let existing_items_map = {}; - let existing_items_list = []; - let me = this; - - $.each(me.frm.doc[this.fieldname] || [], function(i, row) { - existing_items_map[row[me.item_fieldname]] = row.name; - existing_items_list.push(row[me.item_fieldname]); - }); - - // remove unchecked items - $.each(opts.unchecked_items, function(i, item) { - if(existing_items_list.indexOf(item)!=-1) { - frappe.model.clear_doc(me.doctype, existing_items_map[item]); - } - }); - - // add new items that are checked - $.each(opts.checked_items, function(i, item) { - if(existing_items_list.indexOf(item)==-1) { - let row = frappe.model.add_child(me.frm.doc, me.doctype, me.fieldname); - row[me.item_fieldname] = item; - } - }); - - refresh_field(this.fieldname); - } -}); \ No newline at end of file diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js index 3cc34e3f79..30303f61b2 100644 --- a/frappe/public/js/frappe/db.js +++ b/frappe/public/js/frappe/db.js @@ -2,6 +2,27 @@ // MIT License. See license.txt frappe.db = { + get_list: function(doctype, args) { + if (!args) { + args = {}; + } + args.doctype = doctype; + if (!args.fields) { + args.fields = ['name']; + } + if (!args.limit) { + args.limit = 20; + } + return new Promise ((resolve) => { + frappe.call({ + method: 'frappe.model.db_query.get_list', + args: args, + callback: function(r) { + resolve(r.message); + } + }); + }); + }, exists: function(doctype, name) { return new Promise ((resolve) => { frappe.db.get_value(doctype, {name: name}, 'name').then((r) => { diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index b4ceac66bd..ec7a4b9e06 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -10,6 +10,11 @@ frappe.dom = { by_id: function(id) { return document.getElementById(id); }, + get_unique_id: function() { + const id = 'unique-' + frappe.dom.id_count; + frappe.dom.id_count++; + return id; + }, set_unique_id: function(ele) { var $ele = $(ele); if($ele.attr('id')) { diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index b898860f23..9a4c5ca5b5 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -2,30 +2,32 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ make_input() { this._super(); this.setup_awesomplete(); + this.set_options(); }, - setup_awesomplete() { - var me = this; + set_options() { + if (this.df.options) { + let options = this.df.options || []; + if(typeof options === 'string') { + options = options.split('\n'); + } + this._data = options; + } + }, - this.awesomplete = new Awesomplete(this.input, { + get_awesomplete_settings() { + return { minChars: 0, maxItems: 99, autoFirst: true, - list: this.get_data(), - data: function (item) { - if (typeof item === 'string') { - item = { - label: item, - value: item - }; - } - - return { - label: item.label || item.value, - value: item.value - }; - } - }); + list: this.get_data() + }; + }, + + setup_awesomplete() { + var me = this; + + this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings()); $(this.input_area).find('.awesomplete ul').css('min-width', '100%'); diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 68a1138584..21028e1e25 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -148,10 +148,8 @@ frappe.ui.form.Control = Class.extend({ 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; + return this.value || undefined; } }, set_model_value: function(value) { diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 99edde9936..e82690de30 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -38,6 +38,7 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ } else { this.label_area = this.label_span = this.$wrapper.find("label").get(0); this.input_area = this.$wrapper.find(".control-input").get(0); + this.$input_wrapper = this.$wrapper.find(".control-input-wrapper"); // keep a separate display area to rendered formatted values // like links, currencies, HTMLs etc. this.disp_area = this.$wrapper.find(".control-value").get(0); diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js new file mode 100644 index 0000000000..7dd0c7f64e --- /dev/null +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -0,0 +1,184 @@ +frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlCode.extend({ + make_wrapper() { + // Create the elements for map area + this._super(); + + let $input_wrapper = this.$wrapper.find('.control-input-wrapper'); + this.map_id = frappe.dom.get_unique_id(); + this.map_area = $( + `
    +
    +
    ` + ); + this.map_area.prependTo($input_wrapper); + this.$wrapper.find('.control-input').addClass("hidden"); + this.bind_leaflet_map(); + this.bind_leaflet_draw_control(); + this.bind_leaflet_locate_control(); + this.bind_leaflet_refresh_button(); + }, + + format_for_input(value) { + // render raw value from db into map + this.clear_editable_layers(); + if(value) { + var data_layers = new L.FeatureGroup() + .addLayer(L.geoJson(JSON.parse(value),{ + pointToLayer: function(geoJsonPoint, latlng) { + if (geoJsonPoint.properties.point_type == "circle"){ + return L.circle(latlng, {radius: geoJsonPoint.properties.radius}); + } else if (geoJsonPoint.properties.point_type == "circlemarker") { + return L.circleMarker(latlng, {radius: geoJsonPoint.properties.radius}); + } + else { + return L.marker(latlng); + } + } + })); + this.add_non_group_layers(data_layers, this.editableLayers); + try { + this.map.flyToBounds(this.editableLayers.getBounds(), { + padding: [50,50] + }); + } + catch(err) { + // suppress error if layer has a point. + } + this.editableLayers.addTo(this.map); + this.map._onResize(); + } else if ((value===undefined) || (value == JSON.stringify(new L.FeatureGroup().toGeoJSON()))) { + this.locate_control.start(); + } + }, + + bind_leaflet_map() { + + var circleToGeoJSON = L.Circle.prototype.toGeoJSON; + L.Circle.include({ + toGeoJSON: function() { + var feature = circleToGeoJSON.call(this); + feature.properties = { + point_type: 'circle', + radius: this.getRadius() + }; + return feature; + } + }); + + L.CircleMarker.include({ + toGeoJSON: function() { + var feature = circleToGeoJSON.call(this); + feature.properties = { + point_type: 'circlemarker', + radius: this.getRadius() + }; + return feature; + } + }); + + L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; + this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13); + + L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(this.map); + }, + + bind_leaflet_locate_control() { + // To request location update and set location, sets current geolocation on load + this.locate_control = L.control.locate({position:'topright'}); + this.locate_control.addTo(this.map); + }, + + bind_leaflet_draw_control() { + this.editableLayers = new L.FeatureGroup(); + + var options = { + position: 'topleft', + draw: { + polyline: { + shapeOptions: { + color: frappe.ui.color.get('blue'), + weight: 10 + } + }, + polygon: { + allowIntersection: false, // Restricts shapes to simple polygons + drawError: { + color: frappe.ui.color.get('orange'), // Color the shape will turn when intersects + message: 'Oh snap! you can\'t draw that!' // Message that will show when intersect + }, + shapeOptions: { + color: frappe.ui.color.get('blue') + } + }, + circle: true, + rectangle: { + shapeOptions: { + clickable: false + } + } + }, + edit: { + featureGroup: this.editableLayers, //REQUIRED!! + remove: true + } + }; + + // create control and add to map + var drawControl = new L.Control.Draw(options); + + this.map.addControl(drawControl); + + this.map.on('draw:created', (e) => { + var type = e.layerType, + layer = e.layer; + if (type === 'marker') { + layer.bindPopup('Marker'); + } + this.editableLayers.addLayer(layer); + this.set_value(JSON.stringify(this.editableLayers.toGeoJSON())); + }); + + this.map.on('draw:deleted draw:edited', (e) => { + var layer = e.layer; + this.editableLayers.removeLayer(layer); + this.set_value(JSON.stringify(this.editableLayers.toGeoJSON())); + }); + }, + + bind_leaflet_refresh_button() { + L.easyButton({ + id: 'refresh-map-'+this.df.fieldname, + position: 'topright', + type: 'replace', + leafletClasses: true, + states:[{ + stateName: 'refresh-map', + onClick: function(button, map){ + map._onResize(); + }, + title: 'Refresh map', + icon: 'fa fa-refresh' + }] + }).addTo(this.map); + }, + + add_non_group_layers(source_layer, target_group) { + // https://gis.stackexchange.com/a/203773 + // Would benefit from https://github.com/Leaflet/Leaflet/issues/4461 + if (source_layer instanceof L.LayerGroup) { + source_layer.eachLayer((layer)=>{ + this.add_non_group_layers(layer, target_group); + }); + } else { + target_group.addLayer(source_layer); + } + }, + + clear_editable_layers() { + this.editableLayers.eachLayer((l)=>{ + this.editableLayers.removeLayer(l); + }); + } +}); diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 86bf0032fd..9da87931e2 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -44,11 +44,6 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ 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; diff --git a/frappe/public/js/frappe/form/controls/multicheck.js b/frappe/public/js/frappe/form/controls/multicheck.js new file mode 100644 index 0000000000..63ac821d81 --- /dev/null +++ b/frappe/public/js/frappe/form/controls/multicheck.js @@ -0,0 +1,145 @@ +frappe.ui.form.ControlMultiCheck = frappe.ui.form.Control.extend({ + // UI: multiple checkboxes + // Value: Array of values + // Options: Array of label/value/checked option objects + + make() { + this._super(); + this.$label = $(``).appendTo(this.wrapper); + this.$load_state = $('
    ' + __("Loading") + '...
    '); + this.$select_buttons = this.get_select_buttons().appendTo(this.wrapper); + this.$load_state.appendTo(this.wrapper); + + const row = this.get_column_size() === 12 ? '' : 'row'; + this.$checkbox_area = $(`
    `).appendTo(this.wrapper); + this.set_options(); + this.bind_checkboxes(); + }, + + refresh_input() { + this.options.map(option => option.value).forEach(value => { + $(this.wrapper) + .find(`:checkbox[data-unit="${value}"]`) + .prop("checked", this.selected_options.includes(value)); + }); + }, + + set_options() { + this.$load_state.show(); + this.$select_buttons.hide(); + this.parse_df_options(); + + if(this.df.get_data) { + if(typeof this.df.get_data().then == 'function') { + this.df.get_data().then(results => { + this.options = results; + this.make_checkboxes(); + }); + } else { + this.options = this.df.get_data(); + this.make_checkboxes(); + } + } else { + this.make_checkboxes(); + } + }, + + parse_df_options() { + if(Array.isArray(this.df.options)) { + this.options = this.df.options; + } else if(this.df.options && this.df.options.length>0 && frappe.utils.is_json(this.df.options)) { + let args = JSON.parse(this.df.options); + if(Array.isArray(args)) { + this.options = args; + } else if(Array.isArray(args.options)) { + if(args.select_all) { + this.select_all = true; + } + this.options = args.options; + } + } else { + this.options = []; + } + }, + + make_checkboxes() { + this.set_checked_options(); + this.$load_state.hide(); + this.$checkbox_area.empty(); + this.options.forEach(option => { + this.get_checkbox_element(option).appendTo(this.$checkbox_area); + }); + if(this.select_all) { + this.setup_select_all(); + } + }, + + bind_checkboxes() { + $(this.wrapper).on('change', ':checkbox', e => { + const $checkbox = $(e.target); + const option_name = $checkbox.attr("data-unit"); + if($checkbox.is(':checked')) { + this.selected_options.push(option_name); + } else { + let index = this.selected_options.indexOf(option_name); + this.selected_options.splice(index, 1); + } + this.df.on_change && this.df.on_change(); + }); + }, + + set_checked_options() { + this.selected_options = this.options + .filter(o => o.checked) + .map(o => o.value); + }, + + setup_select_all() { + this.$select_buttons.show(); + let select_all = (deselect=false) => { + $(this.wrapper).find(`:checkbox`).prop("checked", deselect).trigger('click'); + }; + this.$select_buttons.find('.select-all').on('click', () => { + select_all(); + }); + this.$select_buttons.find('.deselect-all').on('click', () => { + select_all(true); + }); + }, + + get_value() { + return this.selected_options; + }, + + get_checked_options() { + return this.get_value(); + }, + + get_unchecked_options() { + return this.options.map(o => o.value) + .filter(value => !this.selected_options.includes(value)); + }, + + get_checkbox_element(option) { + const column_size = this.get_column_size(); + return $(` +
    + +
    `); + }, + + get_select_buttons() { + return $(`
    +
    `); + }, + + get_column_size() { + return 12 / (+this.df.columns || 1); + } +}); diff --git a/frappe/public/js/frappe/form/controls/multiselect.js b/frappe/public/js/frappe/form/controls/multiselect.js new file mode 100644 index 0000000000..4b6984595e --- /dev/null +++ b/frappe/public/js/frappe/form/controls/multiselect.js @@ -0,0 +1,29 @@ +frappe.ui.form.ControlMultiSelect = frappe.ui.form.ControlAutocomplete.extend({ + get_awesomplete_settings() { + const settings = this._super(); + + return Object.assign(settings, { + filter: function(text, input) { + return Awesomplete.FILTER_CONTAINS(text, input.match(/[^,]*$/)[0]); + }, + + item: function(text, input) { + return Awesomplete.ITEM(text, input.match(/[^,]*$/)[0]); + }, + + replace: function(text) { + const before = this.input.value.match(/^.+,\s*|/)[0]; + this.input.value = before + text + ", "; + } + }); + }, + + get_data() { + const value = this.get_value() || ''; + const values = value.split(', ').filter(d => d); + const data = this._super(); + + // return values which are not already selected + return data.filter(d => !values.includes(d)); + } +}); diff --git a/frappe/public/js/frappe/form/controls/password.js b/frappe/public/js/frappe/form/controls/password.js index 8a25642737..f16e89bd3d 100644 --- a/frappe/public/js/frappe/form/controls/password.js +++ b/frappe/public/js/frappe/form/controls/password.js @@ -22,7 +22,7 @@ frappe.ui.form.ControlPassword = frappe.ui.form.ControlData.extend({ get_password_strength: function(value) { var me = this; frappe.call({ - type: 'GET', + type: 'POST', method: 'frappe.core.doctype.user.user.test_password_strength', args: { new_password: value || '' diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 9c84a07af0..46d1ff8691 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -334,13 +334,14 @@ frappe.ui.form.Dashboard = Class.extend({ // heatmap render_heatmap: function() { if(!this.heatmap) { - this.heatmap = new frappe.chart.FrappeChart({ + this.heatmap = new Chart({ parent: "#heatmap-" + frappe.model.scrub(this.frm.doctype), type: 'heatmap', height: 100, start: new Date(moment().subtract(1, 'year').toDate()), - count_label: frappe.model.scrub(this.frm.doctype) + "s", - discrete_domains: 0 + count_label: "interactions", + discrete_domains: 0, + data: {} }); // center the heatmap @@ -408,11 +409,12 @@ frappe.ui.form.Dashboard = Class.extend({ $.extend(args, { parent: '.form-graph', type: 'line', - height: 140 + height: 140, + colors: ['green'] }); this.show(); - this.chart = new frappe.chart.FrappeChart(args); + this.chart = new Chart(args); if(!this.chart) { this.hide(); } diff --git a/frappe/public/js/frappe/form/footer/assign_to.js b/frappe/public/js/frappe/form/footer/assign_to.js index 950443de57..80aa38369f 100644 --- a/frappe/public/js/frappe/form/footer/assign_to.js +++ b/frappe/public/js/frappe/form/footer/assign_to.js @@ -136,12 +136,12 @@ frappe.ui.form.AssignToDialog = Class.extend({ {fieldtype: 'Link', fieldname:'assign_to', options:'User', label:__("Assign To"), reqd:true, filters: {'user_type': 'System User'}}, {fieldtype:'Check', fieldname:'myself', label:__("Assign to me"), "default":0}, - {fieldtype:'Small Text', fieldname:'description', label:__("Comment"), reqd:true}, + {fieldtype:'Small Text', fieldname:'description', label:__("Comment")}, {fieldtype: 'Section Break'}, {fieldtype: 'Column Break'}, {fieldtype:'Date', fieldname:'date', label: __("Complete By")}, {fieldtype:'Check', fieldname:'notify', - label:__("Notify by Email"), "default":1}, + label:__("Notify by Email")}, {fieldtype: 'Column Break'}, {fieldtype:'Select', fieldname:'priority', label: __("Priority"), options:[ diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 418b8f277e..24e19c7fc5 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -51,6 +51,10 @@ frappe.ui.form.Timeline = Class.extend({ var communications = me.get_communications().concat(new_communications); frappe.model.set_docinfo(me.frm.doc.doctype, me.frm.doc.name, "communications", communications); + if (new_communications.length < 20) { + me.more = false; + } + } else { me.more = false; } @@ -608,15 +612,10 @@ frappe.ui.form.Timeline = Class.extend({ */ update_comment: function(name, content) { - // TODO: is there a frappe.client.update function? return frappe.call({ - method: 'frappe.client.set_value', - args: { - doctype: 'Communication', - name: name, - fieldname: 'content', - value: content, - }, callback: function(r) { + method: 'frappe.desk.form.utils.update_comment', + args: { name, content }, + callback: function(r) { if(!r.exc) { frappe.utils.play_sound('click'); } diff --git a/frappe/public/js/frappe/form/form_sidebar.js b/frappe/public/js/frappe/form/form_sidebar.js index 989bdfbca4..dfe8abb6b5 100644 --- a/frappe/public/js/frappe/form/form_sidebar.js +++ b/frappe/public/js/frappe/form/form_sidebar.js @@ -70,10 +70,10 @@ frappe.ui.form.Sidebar = Class.extend({ }, refresh_comments: function() { - var comments = $.map(this.frm.timeline.get_communications(), function(c) { + $.map(this.frm.timeline.get_communications(), function(c) { return (c.communication_type==="Communication" || (c.communication_type=="Comment" && c.comment_type==="Comment")) ? c : null; }); - this.comments.find(".n-comments").html(comments.length); + this.comments.find(".n-comments").html(this.frm.get_docinfo().total_comments); }, make_tags: function() { diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index ae0aad8218..2295cafecc 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -92,7 +92,10 @@ frappe.form.formatters = { } if(frappe.form.link_formatters[doctype]) { - value = frappe.form.link_formatters[doctype](value, doc); + // don't apply formatters in case of composite (parent field of same type) + if (doc && doctype !== doc.doctype) { + value = frappe.form.link_formatters[doctype](value, doc); + } } if(!value) { diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index b9987b38e8..63710c959f 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -311,7 +311,7 @@ frappe.ui.form.Grid = Class.extend({ } new Sortable($rows.get(0), { - group: {name: 'row'}, + group: {name: me.df.fieldname}, handle: '.sortable-handle', draggable: '.grid-row', filter: 'li, a', @@ -361,7 +361,7 @@ frappe.ui.form.Grid = Class.extend({ }, set_editable_grid_column_disp: function(fieldname, show) { //Hide columns for editable grids - if (this.meta.editable_grid) { + if (this.meta.editable_grid && this.grid_rows) { this.grid_rows.forEach(function(row) { row.columns_list.forEach(function(column) { //Hide the column specified @@ -480,7 +480,7 @@ frappe.ui.form.Grid = Class.extend({ }, setup_visible_columns: function() { - if(this.visible_columns) return; + if (this.visible_columns) return; var total_colsize = 1, fields = this.editable_fields || this.docfields; @@ -498,22 +498,6 @@ frappe.ui.form.Grid = Class.extend({ && (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm) && !in_list(frappe.model.layout_fields, df.fieldtype)) { - if(df.columns) { - df.colsize=df.columns; - } - else { - var colsize=2; - switch(df.fieldtype) { - case"Text": - case"Small Text": - colsize=3; - break; - case"Check": - colsize=1 - } - df.colsize=colsize; - } - if(df.columns) { df.colsize=df.columns; } @@ -611,7 +595,7 @@ frappe.ui.form.Grid = Class.extend({ me.frm.clear_table(me.df.fieldname); $.each(data, function(i, row) { - if(i > 4) { + if(i > 6) { var blank_row = true; $.each(row, function(ci, value) { if(value) { @@ -659,12 +643,19 @@ frappe.ui.form.Grid = Class.extend({ data.push([]); data.push([]); data.push([]); + data.push([__("The CSV format is case sensitive")]); + data.push([__("Do not edit headers which are preset in the template")]); data.push(["------"]); $.each(frappe.get_meta(me.df.options).fields, function(i, df) { - if(frappe.model.is_value_type(df.fieldtype)) { + // don't include the hidden field in the template + if(frappe.model.is_value_type(df.fieldtype) && !df.hidden) { data[1].push(df.label); data[2].push(df.fieldname); - data[3].push(df.description || ""); + let description = (df.description || "") + ' '; + if (df.fieldtype === "Date") { + description += frappe.boot.sysdefaults.date_format; + } + data[3].push(description); docfields.push(df); } }); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index ea04faa865..9b559a644b 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -239,6 +239,26 @@ frappe.ui.form.Layout = Class.extend({ }); }, + refresh_fields: function(fields) { + let fieldnames = fields.map((field) => { + if(field.fieldname) return field.fieldname; + }); + + this.fields_list.map(fieldobj => { + if(fieldnames.includes(fieldobj.df.fieldname)) { + fieldobj.refresh(); + if(fieldobj.df["default"]) { + fieldobj.set_input(fieldobj.df["default"]); + } + } + }); + }, + + add_fields: function(fields) { + this.render(fields); + this.refresh_fields(fields); + }, + refresh_section_collapse: function() { if(!this.doc) return; diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index f3f5e55d71..69cf6c517f 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -38,8 +38,9 @@ frappe.ui.form.QuickEntryForm = Class.extend({ }, set_meta_and_mandatory_fields: function(){ + // prepare a list of mandatory and bold fields this.mandatory = $.map(frappe.get_meta(this.doctype).fields, - function(d) { return (d.reqd || d.bold && !d.read_only) ? d : null; }); + function(d) { return (d.reqd || d.bold && !d.read_only) ? $.extend({}, d) : null; }); this.meta = frappe.get_meta(this.doctype); if (!this.doc) { this.doc = frappe.model.get_new_doc(this.doctype, null, null, true); @@ -51,12 +52,13 @@ frappe.ui.form.QuickEntryForm = Class.extend({ return false; } + this.validate_for_prompt_autoname(); + if (this.too_many_mandatory_fields() || this.has_child_table() || !this.mandatory.length) { return false; } - this.validate_for_prompt_autoname(); return true; }, @@ -79,7 +81,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ validate_for_prompt_autoname: function(){ if(this.meta.autoname && this.meta.autoname.toLowerCase()==='prompt') { - this.mandatory = [{fieldname:'__name', label:__('{0} Name', [this.meta.name]), + this.mandatory = [{fieldname:'__newname', label:__('{0} Name', [this.meta.name]), reqd: 1, fieldtype:'Data'}].concat(this.mandatory); } }, @@ -91,8 +93,6 @@ frappe.ui.form.QuickEntryForm = Class.extend({ fields: this.mandatory, }); this.dialog.doc = this.doc; - // refresh dependencies etc - this.dialog.refresh(); this.register_primary_action(); this.render_edit_in_full_page_link(); @@ -110,6 +110,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ this.dialog.onhide = () => frappe.quick_entry = null; this.dialog.show(); + this.dialog.refresh_dependency(); this.set_defaults(); if (this.init_callback) { @@ -152,7 +153,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ if(me.after_insert) { me.after_insert(me.dialog.doc); } else { - me.open_from_if_not_list(); + me.open_form_if_not_list(); } } }, @@ -168,11 +169,13 @@ frappe.ui.form.QuickEntryForm = Class.extend({ }); }, - open_from_if_not_list: function() { + open_form_if_not_list: function() { let route = frappe.get_route(); let doc = this.dialog.doc; - if(route && !(route[0]==='List' && route[1]===doc.doctype)) { - frappe.set_route('Form', doc.doctype, doc.name); + if (route && !(route[0]==='List' && route[1]===doc.doctype)) { + frappe.run_serially([ + () => frappe.set_route('Form', doc.doctype, doc.name) + ]); } }, @@ -180,12 +183,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({ var me = this; var data = this.dialog.get_values(true); $.each(data, function(key, value) { - if(key==='__name') { - me.dialog.doc.name = value; - } else { - if(!is_null(value)) { - me.dialog.doc[key] = value; - } + if(!is_null(value)) { + me.dialog.doc[key] = value; } }); return this.dialog.doc; @@ -199,8 +198,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({ render_edit_in_full_page_link: function(){ var me = this; - var $link = $('
    ' + - __("Ctrl+enter to save") + ' | ' + __("Edit in full page") + '
    ').appendTo(this.dialog.body); + var $link = $('
    ' + + '
    ').appendTo(this.dialog.body); $link.find('.edit-full').on('click', function() { // edit in form diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index a3954dfac0..06230be561 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -264,7 +264,10 @@ frappe.ui.form.Toolbar = Class.extend({ status = "Submit"; } else if (this.can_save()) { if (!this.frm.save_disabled) { - status = "Save"; + //Show the save button if there is no workflow or if there is a workflow and there are changes + if (this.has_workflow() ? this.frm.doc.__unsaved : true) { + status = "Save"; + } } } else if (this.can_update()) { status = "Update"; diff --git a/frappe/public/js/frappe/list/list_renderer.js b/frappe/public/js/frappe/list/list_renderer.js index cd844f6238..86dd760a1a 100644 --- a/frappe/public/js/frappe/list/list_renderer.js +++ b/frappe/public/js/frappe/list/list_renderer.js @@ -74,6 +74,13 @@ frappe.views.ListRenderer = Class.extend({ should_refresh: function() { return this.list_view.current_view !== this.list_view.last_view; }, + load_last_view: function() { + // this function should handle loading the last view of your list_renderer, + // If you have a last view (for e.g last kanban board in Kanban View), + // load it using frappe.set_route and return true + // else return false + return false; + }, set_wrapper: function () { this.wrapper = this.list_view.wrapper && this.list_view.wrapper.find('.result-list'); }, @@ -318,7 +325,8 @@ frappe.views.ListRenderer = Class.extend({ } var link = $(this).parent().find('a.list-id').get(0); - window.location.href = link.href; + if ( link && link.href ) + window.location.href = link.href; return false; }); }, @@ -348,6 +356,30 @@ frappe.views.ListRenderer = Class.extend({ this.render_tags($item_container, value); }); + this.render_count(); + }, + + render_count: function() { + const $header_right = this.list_view.list_header.find('.list-item__content--activity'); + const current_count = this.list_view.data.length; + + frappe.call({ + method: 'frappe.model.db_query.get_count', + args: { + doctype: this.doctype, + filters: this.list_view.get_filters_args() + } + }).then(r => { + const count = r.message || current_count; + const str = __('{0} of {1}', [current_count, count]); + const $html = $(`${str}`); + + $html.css({ + marginRight: '10px' + }) + $header_right.addClass('text-muted'); + $header_right.html($html); + }) }, // returns html for a data item, diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 8d72b24dd4..5f6419042a 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -27,6 +27,7 @@ frappe.views.ListSidebar = Class.extend({ this.setup_assigned_to_me(); this.setup_views(); this.setup_kanban_boards(); + this.setup_calendar_view(); this.setup_email_inbox(); let limits = frappe.boot.limits; @@ -271,6 +272,45 @@ frappe.views.ListSidebar = Class.extend({ } }); }, + setup_calendar_view: function() { + const doctype = this.doctype; + + frappe.db.get_list('Calendar View', { + filters: { + reference_doctype: doctype + } + }).then(result => { + if (!result) return; + const calendar_views = result; + const $link_calendar = this.sidebar.find('.list-link[data-view="Calendar"]'); + + let default_link = ''; + if (frappe.views.calendar[this.doctype]) { + // has standard calendar view + default_link = `
  • + ${ __("Default") }
  • `; + } + const other_links = calendar_views.map( + calendar_view => `
  • + ${ __(calendar_view.name) } +
  • ` + ).join(''); + + const dropdown_html = ` +
    + + +
    + `; + $link_calendar.removeClass('hide'); + $link_calendar.html(dropdown_html); + }); + }, setup_email_inbox: function() { // get active email account for the user and add in dropdown if(this.doctype != "Communication") diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 42f2bd3aa2..881c1b62ca 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -399,6 +399,13 @@ frappe.views.ListView = frappe.ui.BaseList.extend({ if (this.list_renderer.should_refresh()) { this.setup_list_renderer(); + + if (this.list_renderer.load_last_view && this.list_renderer.load_last_view()) { + // let the list_renderer load the last view for the current view + // for e.g last kanban board for kanban view + return; + } + this.refresh_surroundings(); this.dirty = true; } @@ -594,8 +601,8 @@ frappe.views.ListView = frappe.ui.BaseList.extend({ if (frappe.model.can_import(this.doctype)) { this.page.add_menu_item(__('Import'), function () { - frappe.set_route('data-import-tool', { - doctype: me.doctype + frappe.set_route('List', 'Data Import', { + reference_doctype: me.doctype }); }, true); } diff --git a/frappe/public/js/frappe/misc/utils.js b/frappe/public/js/frappe/misc/utils.js index 0b055ae6f5..8fcc17a9c6 100644 --- a/frappe/public/js/frappe/misc/utils.js +++ b/frappe/public/js/frappe/misc/utils.js @@ -41,6 +41,14 @@ frappe.utils = { is_md: function() { return $(document).width() < 1199 && $(document).width() >= 991; }, + is_json: function(str) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; + }, strip_whitespace: function(html) { return (html || "").replace(/

    \s*<\/p>/g, "").replace(/
    (\s*
    \s*)+/g, "

    "); }, diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 81f0249480..f902216764 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -67,6 +67,9 @@ $.extend(frappe.model, { }, is_value_type: function(fieldtype) { + if (typeof fieldtype == 'object') { + fieldtype = fieldtype.fieldtype; + } // not in no-value type return frappe.model.no_value_type.indexOf(fieldtype)===-1; }, diff --git a/frappe/public/js/frappe/model/workflow.js b/frappe/public/js/frappe/model/workflow.js index bd442f191b..fcbf1931a3 100644 --- a/frappe/public/js/frappe/model/workflow.js +++ b/frappe/public/js/frappe/model/workflow.js @@ -56,7 +56,7 @@ frappe.workflow = { var state = doc[state_fieldname] || frappe.workflow.get_default_state(doctype, doc.docstatus); - var allow_edit = state ? frappe.workflow.get_document_state(doctype, state).allow_edit : null; + var allow_edit = state ? frappe.workflow.get_document_state(doctype, state) && frappe.workflow.get_document_state(doctype, state).allow_edit : null; if(!frappe.user_roles.includes(allow_edit)) { return true; diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 4a941a412c..a35963c1df 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -1,9 +1,10 @@ frappe.RoleEditor = Class.extend({ - init: function(wrapper, frm) { + init: function(wrapper, frm, disable) { var me = this; this.frm = frm; this.wrapper = wrapper; - $(wrapper).html('

    ' + __("Loading") + '...
    ') + this.disable = disable; + $(wrapper).html('
    ' + __("Loading") + '...
    '); return frappe.call({ method: 'frappe.core.doctype.user.user.get_all_roles', callback: function(r) { @@ -21,33 +22,35 @@ frappe.RoleEditor = Class.extend({ show_roles: function() { var me = this; $(this.wrapper).empty(); - var role_toolbar = $('

    \ -

    ').appendTo($(this.wrapper)); - - role_toolbar.find(".btn-add") - .html(__('Add all roles')) - .on("click", function () { - $(me.wrapper).find('input[type="checkbox"]').each(function (i, check) { - if (!$(check).is(":checked")) { - check.checked = true; - } + if(me.frm.doctype != 'User') { + var role_toolbar = $('

    \ +

    ').appendTo($(this.wrapper)); + + role_toolbar.find(".btn-add") + .html(__('Add all roles')) + .on("click", function () { + $(me.wrapper).find('input[type="checkbox"]').each(function (i, check) { + if (!$(check).is(":checked")) { + check.checked = true; + } + }); }); - }); - - role_toolbar.find(".btn-remove") - .html(__('Clear all roles')) - .on("click", function() { - $(me.wrapper).find('input[type="checkbox"]').each(function(i, check) { - if($(check).is(":checked")) { - check.checked = false; - } + + role_toolbar.find(".btn-remove") + .html(__('Clear all roles')) + .on("click", function() { + $(me.wrapper).find('input[type="checkbox"]').each(function(i, check) { + if($(check).is(":checked")) { + check.checked = false; + } + }); }); - }); + } $.each(this.roles, function(i, role) { $(me.wrapper).append(repl('
    \ - \ + \ %(role_display)s\
    ', {role_value: role,role_display:__(role)})); }); @@ -57,12 +60,13 @@ frappe.RoleEditor = Class.extend({ me.frm.dirty(); }); $(this.wrapper).find('.user-role a').click(function() { - me.show_permissions($(this).parent().attr('data-user-role')) + me.show_permissions($(this).parent().attr('data-user-role')); return false; }); }, show: function() { var me = this; + $('.box').attr('disabled', this.disable); // uncheck all roles $(this.wrapper).find('input[type="checkbox"]') @@ -122,13 +126,13 @@ frappe.RoleEditor = Class.extend({ return { checked_roles: checked_roles, unchecked_roles: unchecked_roles - } + }; }, show_permissions: function(role) { // show permissions for a role var me = this; if(!this.perm_dialog) - this.make_perm_dialog() + this.make_perm_dialog(); $(this.perm_dialog.body).empty(); return frappe.call({ method: 'frappe.core.doctype.user.user.get_perm_info', @@ -187,7 +191,7 @@ frappe.RoleEditor = Class.extend({ // %(print)s\ // %(email)s' + '%(set_user_permissions)s\ - ', perm)) + ', perm)); } me.perm_dialog.show(); diff --git a/frappe/public/js/frappe/ui/charts.js b/frappe/public/js/frappe/ui/charts.js deleted file mode 100644 index 8927f2e21b..0000000000 --- a/frappe/public/js/frappe/ui/charts.js +++ /dev/null @@ -1,1556 +0,0 @@ -// specific_values = [ -// { -// title: "Average", -// line_type: "dashed", // "dashed" or "solid" -// value: 10 -// }, - -// summary = [ -// { -// title: "Total", -// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange', -// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue' -// value: 80 -// } -// ] - -// Validate all arguments, check passed data format, set defaults - -frappe.provide("frappe.chart"); - -frappe.chart.FrappeChart = class { - constructor({ - parent = "", - height = 240, - - title = '', subtitle = '', - - data = {}, - format_lambdas = {}, - - specific_values = [], - summary = [], - - is_navigable = 0, - - type = '' - }) { - if(Object.getPrototypeOf(this) === frappe.chart.FrappeChart.prototype) { - if(type === 'line') { - return new frappe.chart.LineChart(arguments[0]); - } else if(type === 'bar') { - return new frappe.chart.BarChart(arguments[0]); - } else if(type === 'percentage') { - return new frappe.chart.PercentageChart(arguments[0]); - } else if(type === 'heatmap') { - return new frappe.chart.HeatMap(arguments[0]); - } - } - - this.parent = document.querySelector(parent); - this.title = title; - this.subtitle = subtitle; - - this.data = data; - this.format_lambdas = format_lambdas; - - this.specific_values = specific_values; - this.summary = summary; - - this.is_navigable = is_navigable; - if(this.is_navigable) { - this.current_index = 0; - } - - this.set_margins(height); - } - - set_margins(height) { - this.base_height = height; - this.height = height - 40; - this.translate_x = 60; - this.translate_y = 10; - } - - setup() { - this.bind_window_events(); - this.refresh(); - } - - bind_window_events() { - window.addEventListener('resize', () => this.refresh()); - window.addEventListener('orientationchange', () => this.refresh()); - } - - refresh() { - this.setup_base_values(); - this.set_width(); - - this.setup_container(); - this.setup_components(); - - this.setup_values(); - this.setup_utils(); - - this.make_graph_components(); - this.make_tooltip(); - - if(this.summary.length > 0) { - this.show_custom_summary(); - } else { - this.show_summary(); - } - - if(this.is_navigable) { - this.setup_navigation(); - } - } - - set_width() { - let special_values_width = 0; - this.specific_values.map(val => { - if(this.get_strwidth(val.title) > special_values_width) { - special_values_width = this.get_strwidth(val.title); - } - }); - this.base_width = this.parent.offsetWidth - special_values_width; - this.width = this.base_width - this.translate_x * 2; - } - - setup_base_values() {} - - setup_container() { - this.container = $$.create('div', { - className: 'chart-container', - innerHTML: `
    ${this.title}
    -
    ${this.subtitle}
    -
    -
    ` - }); - - // Chart needs a dedicated parent element - this.parent.innerHTML = ''; - this.parent.appendChild(this.container); - - this.chart_wrapper = this.container.querySelector('.frappe-chart'); - // this.chart_wrapper.appendChild(); - - this.make_chart_area(); - this.make_draw_area(); - - this.stats_wrapper = this.container.querySelector('.graph-stats-container'); - } - - make_chart_area() { - this.svg = $$.createSVG('svg', { - className: 'chart', - inside: this.chart_wrapper, - width: this.base_width, - height: this.base_height - }); - - return this.svg; - } - - make_draw_area() { - this.draw_area = $$.createSVG("g", { - className: this.type, - inside: this.svg, - transform: `translate(${this.translate_x}, ${this.translate_y})` - }); - } - - setup_components() { - this.svg_units_group = $$.createSVG('g', { - className: 'data-points', - inside: this.draw_area - }); - } - - make_tooltip() { - this.tip = new frappe.chart.SvgTip({ - parent: this.chart_wrapper, - }); - this.bind_tooltip(); - } - - - show_summary() {} - show_custom_summary() { - this.summary.map(d => { - let stats = $$.create('div', { - className: 'stats', - innerHTML: `${d.title}: ${d.value}` - }); - this.stats_wrapper.appendChild(stats); - }); - } - - setup_navigation() { - this.make_overlay(); - this.bind_overlay(); - document.onkeydown = (e) => { - e = e || window.event; - - if (e.keyCode == '37') { - this.on_left_arrow(); - } else if (e.keyCode == '39') { - this.on_right_arrow(); - } else if (e.keyCode == '38') { - this.on_up_arrow(); - } else if (e.keyCode == '40') { - this.on_down_arrow(); - } else if (e.keyCode == '13') { - this.on_enter_key(); - } - }; - } - - make_overlay() {} - bind_overlay() {} - - on_left_arrow() {} - on_right_arrow() {} - on_up_arrow() {} - on_down_arrow() {} - on_enter_key() {} - - get_data_point(index=this.current_index) { - // check for length - let data_point = { - index: index - }; - let y = this.y[0]; - ['svg_units', 'y_tops', 'values'].map(key => { - let data_key = key.slice(0, key.length-1); - data_point[data_key] = y[key][index]; - }); - data_point.label = this.x[index]; - return data_point; - } - - update_current_data_point(index) { - if(index < 0) index = 0; - if(index >= this.x.length) index = this.x.length - 1; - if(index === this.current_index) return; - this.current_index = index; - $$.fire(this.parent, "data-select", this.get_data_point()); - } - - // Helpers - get_strwidth(string) { - return string.length * 8; - } - - // Objects - setup_utils() { - this.draw = { - 'bar': (x, y, args, color, index) => { - let total_width = this.avg_unit_width - args.space_width; - let start_x = x - total_width/2; - - let width = total_width / args.no_of_datasets; - let current_x = start_x + width * index; - if(y == this.height) { - y = this.height * 0.98; - } - return $$.createSVG('rect', { - className: `bar mini fill ${color}`, - x: current_x, - y: y, - width: width, - height: this.height - y - }); - - }, - 'dot': (x, y, args, color) => { - return $$.createSVG('circle', { - className: `fill ${color}`, - cx: x, - cy: y, - r: args.radius - }); - } - }; - - this.animate = { - 'bar': (bar, new_y, args) => { - return [bar, {height: args.new_height, y: new_y}, 300, "easein"]; - // bar.animate({height: args.new_height, y: new_y}, 300, mina.easein); - }, - 'dot': (dot, new_y) => { - return [dot, {cy: new_y}, 300, "easein"]; - // dot.animate({cy: new_y}, 300, mina.easein); - } - }; - } -} - -frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { - constructor(args) { - super(args); - - this.x = this.data.labels; - this.y = this.data.datasets; - - this.get_x_label = this.format_lambdas.x_label; - this.get_y_label = this.format_lambdas.y_label; - this.get_x_tooltip = this.format_lambdas.x_tooltip; - this.get_y_tooltip = this.format_lambdas.y_tooltip; - - this.colors = ['lightblue', 'purple', 'blue', 'green', 'lightgreen', - 'yellow', 'orange', 'red']; - } - - setup_values() { - this.data.datasets.map(d => { - d.values = d.values.map(val => (!isNaN(val) ? val : 0)); - }); - this.setup_x(); - this.setup_y(); - } - - setup_x() { - this.set_avg_unit_width_and_x_offset(); - this.x_axis_values = this.x.map((d, i) => frappe.chart.utils.float_2(this.x_offset + i * this.avg_unit_width)); - } - - setup_y() { - this.setup_metrics(); - this.y_axis_values = this.get_y_axis_values(this.upper_limit, this.parts); - } - - setup_components() { - this.y_axis_group = $$.createSVG('g', {className: 'y axis', inside: this.draw_area}); - this.x_axis_group = $$.createSVG('g', {className: 'x axis', inside: this.draw_area}); - this.specific_y_lines = $$.createSVG('g', {className: 'specific axis', inside: this.draw_area}); - super.setup_components(); - } - - make_graph_components() { - this.make_y_axis(); - this.make_x_axis(); - this.draw_graph(); - this.make_y_specifics(); - } - - // make HORIZONTAL lines for y values - make_y_axis() { - if(this.y_axis_group.textContent) { - // animate from old to new, both elemnets - } else { - // only new - } - - this.y_axis_group.textContent = ''; - - let width, text_end_at = -9, label_class = '', start_at = 0; - if(this.y_axis_mode === 'span') { // long spanning lines - width = this.width + 6; - start_at = -6; - } else if(this.y_axis_mode === 'tick'){ // short label lines - width = -6; - label_class = 'y-axis-label'; - } - - this.y_axis_values.map((point) => { - let line = $$.createSVG('line', { - x1: start_at, - x2: width, - y1: 0, - y2: 0 - }); - let text = $$.createSVG('text', { - className: 'y-value-text', - x: text_end_at, - y: 0, - dy: '.32em', - innerHTML: point+"" - }); - - let y_level = $$.createSVG('g', { - className: `tick ${label_class}`, - transform: `translate(0, ${this.height - point * this.multiplier })` - }); - - y_level.appendChild(line); - y_level.appendChild(text); - - this.y_axis_group.appendChild(y_level); - }); - } - - // make VERTICAL lines for x values - make_x_axis() { - let start_at, height, text_start_at, label_class = ''; - if(this.x_axis_mode === 'span') { // long spanning lines - start_at = -7; - height = this.height + 15; - text_start_at = this.height + 25; - } else if(this.x_axis_mode === 'tick'){ // short label lines - start_at = this.height; - height = 6; - text_start_at = 9; - label_class = 'x-axis-label'; - } - - this.x_axis_group.setAttribute('transform', `translate(0,${start_at})`); - - this.x.map((point, i) => { - let allowed_space = this.avg_unit_width * 1.5; - if(this.get_strwidth(point) > allowed_space) { - let allowed_letters = allowed_space / 8; - point = point.slice(0, allowed_letters-3) + " ..."; - } - - let line = $$.createSVG('line', { - x1: 0, - x2: 0, - y1: 0, - y2: height - }); - let text = $$.createSVG('text', { - className: 'x-value-text', - x: 0, - y: text_start_at, - dy: '.71em', - innerHTML: point - }); - - let x_level = $$.createSVG('g', { - className: `tick ${label_class}`, - transform: `translate(${ this.x_axis_values[i] }, 0)` - }); - - x_level.appendChild(line); - x_level.appendChild(text); - - this.x_axis_group.appendChild(x_level); - }); - } - - draw_graph() { - // TODO: Don't animate on refresh - let data = []; - this.svg_units_group.textContent = ''; - this.y.map((d, i) => { - // Anim: Don't draw initial values, store them and update later - d.y_tops = new Array(d.values.length).fill(this.height); // no value - data.push({values: d.values}); - d.svg_units = []; - - this.make_new_units_for_dataset(d.y_tops, d.color || this.colors[i], i); - this.make_path && this.make_path(d, d.color || this.colors[i]); - }); - - // Data points - // this.calc_all_y_tops(); - // this.calc_min_tops(); - - setTimeout(() => { - this.update_values(data); - }, 500); - } - - setup_navigation() { - // Hack: defer nav till initial update_values - setTimeout(() => { - super.setup_navigation(); - }, 1000); - } - - make_new_units_for_dataset(y_values, color, dataset_index) { - this.y[dataset_index].svg_units = []; - - let d = this.unit_args; - y_values.map((y, i) => { - let data_unit = this.draw[d.type]( - this.x_axis_values[i], - y, - d.args, - color, - dataset_index - ); - this.svg_units_group.appendChild(data_unit); - this.y[dataset_index].svg_units.push(data_unit); - }); - } - - make_y_specifics() { - this.specific_values.map(d => { - let line = $$.createSVG('line', { - className: d.line_type === "dashed" ? "dashed": "", - x1: 0, - x2: this.width, - y1: 0, - y2: 0 - }); - - let text = $$.createSVG('text', { - className: 'specific-value', - x: this.width + 5, - y: 0, - dy: '.32em', - innerHTML: d.title.toUpperCase() - }); - - let specific_y_level = $$.createSVG('g', { - className: `tick`, - transform: `translate(0, ${this.height - d.value * this.multiplier })` - }); - - specific_y_level.appendChild(line); - specific_y_level.appendChild(text); - - this.specific_y_lines.appendChild(specific_y_level); - }); - } - - bind_tooltip() { - // should be w.r.t. this.parent, but will have to take care of - // all the elements and padding, margins on top - this.chart_wrapper.addEventListener('mousemove', (e) => { - let rect = this.chart_wrapper.getBoundingClientRect(); - let offset = { - top: rect.top + document.body.scrollTop, - left: rect.left + document.body.scrollLeft - } - let relX = e.pageX - offset.left - this.translate_x; - let relY = e.pageY - offset.top - this.translate_y; - - if(relY < this.height + this.translate_y * 2) { - this.map_tooltip_x_position_and_show(relX); - } else { - this.tip.hide_tip(); - } - }); - } - - map_tooltip_x_position_and_show(relX) { - for(var i=this.x_axis_values.length - 1; i >= 0 ; i--) { - let x_val = this.x_axis_values[i]; - // let delta = i === 0 ? this.avg_unit_width : x_val - this.x_axis_values[i-1]; - if(relX > x_val - this.avg_unit_width/2) { - let x = x_val + this.translate_x - 0.5; - let y = this.y_min_tops[i] + this.translate_y + 4; // adjustment - - let title = this.x.formatted && this.x.formatted.length>0 - ? this.x.formatted[i] : this.x[i]; - let values = this.y.map((set, j) => { - return { - title: set.title, - value: set.formatted ? set.formatted[i] : set.values[i], - color: set.color || this.colors[j], - } - }); - - this.tip.set_values(x, y, title, '', values); - this.tip.show_tip(); - break; - } - } - } - - // API - update_values(new_y) { - // Just update values prop, setup_y() will do the rest - this.y.map((d, i) => {d.values = new_y[i].values;}); - - let old_upper_limit = this.upper_limit; - this.setup_y(); - if(old_upper_limit !== this.upper_limit){ - this.make_y_axis(); - } - - let elements_to_animate = []; - elements_to_animate = this.animate_for_equilength_data(elements_to_animate); - - // create new x,y pair string and animate path - if(this.y[0].path) { - this.y.map((e, i) => { - let new_points_list = e.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); - let new_path_str = "M"+new_points_list.join("L"); - let args = [{unit:this.y[i].path, object: this.y[i], key:'path'}, {d:new_path_str}, 300, "easein"]; - elements_to_animate.push(args); - }); - } - - // elements_to_animate = elements_to_animate.concat(this.update_y_axis()); - let anim_svg = $$.runSVGAnimation(this.svg, elements_to_animate); - this.chart_wrapper.innerHTML = ''; - this.chart_wrapper.appendChild(anim_svg); - - // Replace the new svg (data has long been replaced) - setTimeout(() => { - this.chart_wrapper.innerHTML = ''; - this.chart_wrapper.appendChild(this.svg); - }, 250); - } - - update_y_axis() { - let elements = []; - - return elements; - } - - update_x_axis() { - // update - } - - animate_for_equilength_data(elements_to_animate) { - this.y.map((d) => { - d.y_tops = d.values.map(val => frappe.chart.utils.float_2(this.height - val * this.multiplier)); - d.svg_units.map((unit, j) => { - elements_to_animate.push(this.animate[this.unit_args.type]( - {unit:unit, array:d.svg_units, index: j}, // unit, with info to replace from data - d.y_tops[j], - {new_height: this.height - d.y_tops[j]} - )); - }); - }); - this.calc_min_tops(); - return elements_to_animate; - } - - add_data_point(data_point) { - this.x.push(data_point.label); - this.y.values.push(); - } - - // Helpers - get_upper_limit_and_parts(array) { - let max_val = parseInt(Math.max(...array)); - if((max_val+"").length <= 1) { - return [10, 5]; - } else { - let multiplier = Math.pow(10, ((max_val+"").length - 1)); - let significant = Math.ceil(max_val/multiplier); - if(significant % 2 !== 0) significant++; - let parts = (significant < 5) ? significant : significant/2; - return [significant * multiplier, parts]; - } - } - - get_y_axis_values(upper_limit, parts) { - let y_axis = []; - for(var i = 0; i <= parts; i++){ - y_axis.push(upper_limit / parts * i); - } - return y_axis; - } - - set_avg_unit_width_and_x_offset() { - this.avg_unit_width = this.width/(this.x.length - 1); - this.x_offset = 0; - } - - setup_metrics() { - // Metrics: upper limit, no. of parts, multiplier - let values = this.get_all_y_values(); - [this.upper_limit, this.parts] = this.get_upper_limit_and_parts(values); - this.multiplier = this.height / this.upper_limit; - } - - get_all_y_values() { - let all_values = []; - this.y.map(d => { - all_values = all_values.concat(d.values); - }); - return all_values.concat(this.specific_values.map(d => d.value)); - } - - calc_all_y_tops() { - this.y.map(d => { - d.y_tops = d.values.map( val => frappe.chart.utils.float_2(this.height - val * this.multiplier)); - }); - } - - calc_min_tops() { - this.y_min_tops = new Array(this.x_axis_values.length).fill(9999); - this.y.map(d => { - d.y_tops.map( (y_top, i) => { - if(y_top < this.y_min_tops[i]) { - this.y_min_tops[i] = y_top; - } - }); - }); - } -} - -frappe.chart.BarChart = class BarChart extends frappe.chart.AxisChart { - constructor() { - super(arguments[0]); - - this.type = 'bar-graph'; - this.setup(); - } - - setup_values() { - super.setup_values(); - this.x_offset = this.avg_unit_width; - this.y_axis_mode = 'span'; - this.x_axis_mode = 'tick'; - this.unit_args = { - type: 'bar', - args: { - space_width: this.avg_unit_width/2, - no_of_datasets: this.y.length - } - }; - } - - make_overlay() { - // Just make one out of the first element - let unit = this.y[0].svg_units[0]; - - this.overlay = unit.cloneNode(); - this.overlay.style.fill = '#000000'; - this.overlay.style.opacity = '0.4'; - this.draw_area.appendChild(this.overlay); - } - - bind_overlay() { - // on event, update overlay - this.parent.addEventListener('data-select', (e) => { - this.update_overlay(e.svg_unit); - }); - } - - update_overlay(unit) { - let attributes = []; - Object.keys(unit.attributes).map(index => { - attributes.push(unit.attributes[index]); - }); - - attributes.filter(attr => attr.specified).map(attr => { - this.overlay.setAttribute(attr.name, attr.nodeValue); - }); - } - - on_left_arrow() { - this.update_current_data_point(this.current_index - 1); - } - - on_right_arrow() { - this.update_current_data_point(this.current_index + 1); - } - - set_avg_unit_width_and_x_offset() { - this.avg_unit_width = this.width/(this.x.length + 1); - this.x_offset = this.avg_unit_width; - } -} - -frappe.chart.LineChart = class LineChart extends frappe.chart.AxisChart { - constructor(args) { - super(args); - if(Object.getPrototypeOf(this) !== frappe.chart.LineChart.prototype) { - return; - } - - this.type = 'line-graph'; - - this.setup(); - } - - setup_values() { - super.setup_values(); - this.y_axis_mode = 'span'; - this.x_axis_mode = 'span'; - this.unit_args = { - type: 'dot', - args: { radius: 4 } - }; - } - - make_path(d, color) { - let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); - let path_str = "M"+points_list.join("L"); - - d.path = $$.createSVG('path', { - className: `stroke ${color}`, - d: path_str - }); - - this.svg_units_group.prepend(d.path); - } -} - -frappe.chart.RegionChart = class RegionChart extends frappe.chart.LineChart { - constructor(args) { - super(args); - - this.type = 'region-graph'; - this.region_fill = 1; - this.setup(); - } -} - -frappe.chart.PercentageChart = class PercentageChart extends frappe.chart.FrappeChart { - constructor(args) { - super(args); - - this.x = this.data.labels; - this.y = this.data.datasets; - - this.get_x_label = this.format_lambdas.x_label; - this.get_y_label = this.format_lambdas.y_label; - this.get_x_tooltip = this.format_lambdas.x_tooltip; - this.get_y_tooltip = this.format_lambdas.y_tooltip; - - this.setup(); - } - - make_chart_area() { - this.chart_wrapper.className += ' ' + 'graph-focus-margin'; - this.chart_wrapper.style.marginTop = '45px'; - - this.stats_wrapper.className += ' ' + 'graph-focus-margin'; - this.stats_wrapper.style.marginBottom = '30px'; - this.stats_wrapper.style.paddingTop = '0px'; - - this.chart_div = $$.create('div', { - className: 'div', - inside: this.chart_wrapper, - width: this.base_width, - height: this.base_height - }); - - this.chart = $$.create('div', { - className: 'progress-chart', - inside: this.chart_div - }); - } - - setup_values() { - this.x.totals = this.x.map((d, i) => { - let total = 0; - this.y.map(e => { - total += e.values[i]; - }); - return total; - }); - - if(!this.x.colors) { - this.x.colors = ['green', 'blue', 'purple', 'red', 'orange', - 'yellow', 'lightblue', 'lightgreen']; - } - } - - setup_utils() { } - setup_components() { - this.percentage_bar = $$.create('div', { - className: 'progress', - inside: this.chart - }); - } - - make_graph_components() { - this.grand_total = this.x.totals.reduce((a, b) => a + b, 0); - this.x.units = []; - this.x.totals.map((total, i) => { - let part = $$.create('div', { - className: `progress-bar background ${this.x.colors[i]}`, - style: `width: ${total*100/this.grand_total}%`, - inside: this.percentage_bar - }); - this.x.units.push(part); - }); - } - - bind_tooltip() { - this.x.units.map((part, i) => { - part.addEventListener('mouseenter', () => { - let g_off = this.chart_wrapper.offset(), p_off = part.offset(); - - let x = p_off.left - g_off.left + part.offsetWidth/2; - let y = p_off.top - g_off.top - 6; - let title = (this.x.formatted && this.x.formatted.length>0 - ? this.x.formatted[i] : this.x[i]) + ': '; - let percent = (this.x.totals[i]*100/this.grand_total).toFixed(1); - - this.tip.set_values(x, y, title, percent); - this.tip.show_tip(); - }); - }); - } - - show_summary() { - let x_values = this.x.formatted && this.x.formatted.length > 0 - ? this.x.formatted : this.x; - this.x.totals.map((d, i) => { - if(d) { - let stats = $$.create('div', { - className: 'stats', - inside: this.stats_wrapper - }); - stats.innerHTML = ` - ${x_values[i]}: - ${d} - `; - } - }); - } -} - -frappe.chart.HeatMap = class HeatMap extends frappe.chart.FrappeChart { - constructor({ - start = new Date(moment().subtract(1, 'year').toDate()), - domain = '', - subdomain = '', - data = {}, - discrete_domains = 0, - count_label = '' - }) { - super(arguments[0]); - - this.type = 'heatmap'; - - this.domain = domain; - this.subdomain = subdomain; - this.start = start; - this.data = data; - this.discrete_domains = discrete_domains; - this.count_label = count_label; - - this.legend_colors = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; - - this.translate_x = 0; - this.setup(); - } - - setup_base_values() { - this.today = new Date(); - - if(!this.start) { - this.start = new Date(); - this.start.setFullYear( this.start.getFullYear() - 1 ); - } - this.first_week_start = new Date(this.start.toDateString()); - this.last_week_start = new Date(this.today.toDateString()); - if(this.first_week_start.getDay() !== 7) { - this.add_days(this.first_week_start, (-1) * this.first_week_start.getDay()); - } - if(this.last_week_start.getDay() !== 7) { - this.add_days(this.last_week_start, (-1) * this.last_week_start.getDay()); - } - this.no_of_cols = this.get_weeks_between(this.first_week_start + '', this.last_week_start + '') + 1; - } - - set_width() { - this.base_width = (this.no_of_cols) * 12; - } - - setup_components() { - this.domain_label_group = $$.createSVG("g", { - className: "domain-label-group chart-label", - inside: this.draw_area - }); - this.data_groups = $$.createSVG("g", { - className: "data-groups", - inside: this.draw_area, - transform: `translate(0, 20)` - }); - } - - setup_values() { - this.domain_label_group.textContent = ''; - this.data_groups.textContent = ''; - this.distribution = this.get_distribution(this.data, this.legend_colors); - this.month_names = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - ]; - - this.render_all_weeks_and_store_x_values(this.no_of_cols); - } - - render_all_weeks_and_store_x_values(no_of_weeks) { - let current_week_sunday = new Date(this.first_week_start); - this.week_col = 0; - this.current_month = current_week_sunday.getMonth(); - - this.months = [this.current_month + '']; - this.month_weeks = {}, this.month_start_points = []; - this.month_weeks[this.current_month] = 0; - this.month_start_points.push(13); - - for(var i = 0; i < no_of_weeks; i++) { - let data_group, month_change = 0; - let day = new Date(current_week_sunday); - - [data_group, month_change] = this.get_week_squares_group(day, this.week_col); - this.data_groups.appendChild(data_group); - this.week_col += 1 + parseInt(this.discrete_domains && month_change); - this.month_weeks[this.current_month]++; - if(month_change) { - this.current_month = (this.current_month + 1) % 12; - this.months.push(this.current_month + ''); - this.month_weeks[this.current_month] = 1; - } - this.add_days(current_week_sunday, 7); - } - this.render_month_labels(); - } - - get_week_squares_group(current_date, index) { - const no_of_weekdays = 7; - const square_side = 10; - const cell_padding = 2; - const step = 1; - - let month_change = 0; - let week_col_change = 0; - - let data_group = $$.createSVG("g", { - className: "data-group", - inside: this.data_groups - }); - - for(var y = 0, i = 0; i < no_of_weekdays; i += step, y += (square_side + cell_padding)) { - let data_value = 0; - let color_index = 0; - - // TODO: More foolproof for any data - let timestamp = Math.floor(current_date.getTime()/1000).toFixed(1); - - if(this.data[timestamp]) { - data_value = this.data[timestamp]; - color_index = this.get_max_checkpoint(data_value, this.distribution); - } - - if(this.data[Math.round(timestamp)]) { - data_value = this.data[Math.round(timestamp)]; - color_index = this.get_max_checkpoint(data_value, this.distribution); - } - - let x = 13 + (index + week_col_change) * 12; - - $$.createSVG("rect", { - className: 'day', - inside: data_group, - x: x, - y: y, - width: square_side, - height: square_side, - fill: this.legend_colors[color_index], - 'data-date': this.get_dd_mm_yyyy(current_date), - 'data-value': data_value, - 'data-day': current_date.getDay() - }); - - let next_date = new Date(current_date); - this.add_days(next_date, 1); - if(next_date.getMonth() - current_date.getMonth()) { - month_change = 1; - if(this.discrete_domains) { - week_col_change = 1; - } - - this.month_start_points.push(13 + (index + week_col_change) * 12); - } - current_date = next_date; - } - - return [data_group, month_change]; - } - - render_month_labels() { - // this.first_month_label = 1; - // if (this.first_week_start.getDate() > 8) { - // this.first_month_label = 0; - // } - // this.last_month_label = 1; - - // let first_month = this.months.shift(); - // let first_month_start = this.month_start_points.shift(); - // render first month if - - // let last_month = this.months.pop(); - // let last_month_start = this.month_start_points.pop(); - // render last month if - - this.months.shift(); - this.month_start_points.shift(); - this.months.pop(); - this.month_start_points.pop(); - - this.month_start_points.map((start, i) => { - let month_name = this.month_names[this.months[i]].substring(0, 3); - - $$.createSVG('text', { - className: 'y-value-text', - inside: this.domain_label_group, - x: start + 12, - y: 10, - dy: '.32em', - innerHTML: month_name - }); - - }); - } - - make_graph_components() { - Array.prototype.slice.call( - this.container.querySelectorAll('.graph-stats-container, .sub-title, .title') - ).map(d => { - d.style.display = 'None'; - }); - this.chart_wrapper.style.marginTop = '0px'; - this.chart_wrapper.style.paddingTop = '0px'; - } - - bind_tooltip() { - Array.prototype.slice.call( - document.querySelectorAll(".data-group .day") - ).map(el => { - el.addEventListener('mouseenter', (e) => { - let count = e.target.getAttribute('data-value'); - let date_parts = e.target.getAttribute('data-date').split('-'); - - let month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3); - - let g_off = this.chart_wrapper.getBoundingClientRect(), p_off = e.target.getBoundingClientRect(); - - let width = parseInt(e.target.getAttribute('width')); - let x = p_off.left - g_off.left + (width+2)/2; - let y = p_off.top - g_off.top - (width+2)/2; - let value = count + ' ' + this.count_label; - let name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2]; - - this.tip.set_values(x, y, name, value, [], 1); - this.tip.show_tip(); - }); - }); - } - - update(data) { - this.data = data; - this.setup_values(); - this.bind_tooltip(); - } - - get_distribution(data={}, mapper_array) { - let data_values = Object.keys(data).map(key => data[key]); - let data_max_value = Math.max(...data_values); - - let distribution_step = 1 / (mapper_array.length - 1); - let distribution = []; - - mapper_array.map((color, i) => { - let checkpoint = data_max_value * (distribution_step * i); - distribution.push(checkpoint); - }); - - return distribution; - } - - get_max_checkpoint(value, distribution) { - return distribution.filter((d, i) => { - if(i === 1) { - return distribution[0] < value; - } - return d <= value; - }).length - 1; - } - - // TODO: date utils, move these out - - // https://stackoverflow.com/a/11252167/6495043 - treat_as_utc(date_str) { - let result = new Date(date_str); - result.setMinutes(result.getMinutes() - result.getTimezoneOffset()); - return result; - } - - get_dd_mm_yyyy(date) { - let dd = date.getDate(); - let mm = date.getMonth() + 1; // getMonth() is zero-based - return [ - (dd>9 ? '' : '0') + dd, - (mm>9 ? '' : '0') + mm, - date.getFullYear() - ].join('-'); - } - - get_weeks_between(start_date_str, end_date_str) { - return Math.ceil(this.get_days_between(start_date_str, end_date_str) / 7); - } - - get_days_between(start_date_str, end_date_str) { - let milliseconds_per_day = 24 * 60 * 60 * 1000; - return (this.treat_as_utc(end_date_str) - this.treat_as_utc(start_date_str)) / milliseconds_per_day; - } - - // mutates - add_days(date, number_of_days) { - date.setDate(date.getDate() + number_of_days); - } - - get_month_name() {} -} - -frappe.chart.SvgTip = class { - constructor({ - parent = null - }) { - this.parent = parent; - this.title_name = ''; - this.title_value = ''; - this.list_values = []; - this.title_value_first = 0; - - this.x = 0; - this.y = 0; - - this.top = 0; - this.left = 0; - - this.setup(); - } - - setup() { - this.make_tooltip(); - } - - refresh() { - this.fill(); - this.calc_position(); - // this.show_tip(); - } - - make_tooltip() { - this.container = $$.create('div', { - className: 'graph-svg-tip comparison', - innerHTML: ` -
      -
      ` - }); - - this.parent.appendChild(this.container); - this.hide_tip(); - - this.title = this.container.querySelector('.title'); - this.data_point_list = this.container.querySelector('.data-point-list'); - - this.parent.addEventListener('mouseleave', () => { - this.hide_tip(); - }); - } - - fill() { - let title; - if(this.title_value_first) { - title = `${this.title_value}${this.title_name}`; - } else { - title = `${this.title_name}${this.title_value}`; - } - this.title.innerHTML = title; - this.data_point_list.innerHTML = ''; - - this.list_values.map((set) => { - let li = $$.create('li', { - className: `border-top ${set.color || 'black'}`, - innerHTML: `${set.value ? set.value : '' } - ${set.title ? set.title : '' }` - }); - - this.data_point_list.appendChild(li); - }); - } - - calc_position() { - this.top = this.y - this.container.offsetHeight; - this.left = this.x - this.container.offsetWidth/2; - let max_left = this.parent.offsetWidth - this.container.offsetWidth; - - let pointer = this.container.querySelector('.svg-pointer'); - - if(this.left < 0) { - pointer.style.left = `calc(50% - ${-1 * this.left}px)`; - this.left = 0; - } else if(this.left > max_left) { - let delta = this.left - max_left; - pointer.style.left = `calc(50% + ${delta}px)`; - this.left = max_left; - } else { - pointer.style.left = `50%`; - } - } - - set_values(x, y, title_name = '', title_value = '', list_values = [], title_value_first = 0) { - this.title_name = title_name; - this.title_value = title_value; - this.list_values = list_values; - this.x = x; - this.y = y; - this.title_value_first = title_value_first; - this.refresh(); - } - - hide_tip() { - this.container.style.top = '0px'; - this.container.style.left = '0px'; - this.container.style.opacity = '0'; - } - - show_tip() { - this.container.style.top = this.top + 'px'; - this.container.style.left = this.left + 'px'; - this.container.style.opacity = '1'; - } -} - -frappe.chart.map_c3 = (chart) => { - if (chart.data) { - let data = chart.data; - let type = chart.chart_type || 'line'; - if(type === 'pie') { - type = 'percentage'; - } - - let x = {}, y = []; - - if(data.columns) { - let columns = data.columns; - - x = columns.filter(col => { - return col[0] === data.x; - })[0]; - - if(x && x.length) { - let dataset_length = x.length; - let dirty = false; - columns.map(col => { - if(col[0] !== data.x) { - if(col.length === dataset_length) { - let title = col[0]; - col.splice(0, 1); - y.push({ - title: title, - values: col, - }); - } else { - dirty = true; - } - } - }); - - if(dirty) { - return; - } - - x.splice(0, 1); - - return { - type: type, - y: y, - x: x - } - - } - } else if(data.rows) { - let rows = data.rows; - x = rows[0]; - - rows.map((row, i) => { - if(i === 0) { - x = row; - } else { - y.push({ - title: 'data' + i, - values: row, - }); - } - }); - - return { - type: type, - y: y, - x: x - } - } - } -} - -// Helpers -frappe.chart.utils = {}; -frappe.chart.utils.float_2 = d => parseFloat(d.toFixed(2)); -function $$(expr, con) { - return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; -} - -// $$.findNodeIndex = (node) => -// { -// var i = 0; -// while (node = node.previousSibling) { -// if (node.nodeType === 1) { ++i; } -// } -// return i; -// } - -$$.create = function(tag, o) { - var element = document.createElement(tag); - - for (var i in o) { - var val = o[i]; - - if (i === "inside") { - $$(val).appendChild(element); - } - else if (i === "around") { - var ref = $$(val); - ref.parentNode.insertBefore(element, ref); - element.appendChild(ref); - } - else if (i in element) { - element[i] = val; - } - else { - element.setAttribute(i, val); - } - } - - return element; -}; - -$$.createSVG = function(tag, o) { - var element = document.createElementNS("http://www.w3.org/2000/svg", tag); - - for (var i in o) { - var val = o[i]; - - if (i === "inside") { - $$(val).appendChild(element); - } - else if (i === "around") { - var ref = $$(val); - ref.parentNode.insertBefore(element, ref); - element.appendChild(ref); - } - else { - if(i === "className") { i = "class"; } - if(i === "innerHTML") { - element['textContent'] = val; - } else { - element.setAttribute(i, val); - } - } - } - - return element; -}; - -$$.runSVGAnimation = (svg_container, elements) => { - let parent = elements[0][0]['unit'].parentNode; - - let new_elements = []; - let anim_elements = []; - - elements.map(element => { - let obj = element[0]; - // let index = $$.findNodeIndex(obj.unit); - - let anim_element, new_element; - - element[0] = obj.unit; - [anim_element, new_element] = $$.animateSVG(...element); - - new_elements.push(new_element); - anim_elements.push(anim_element); - - parent.replaceChild(anim_element, obj.unit); - - if(obj.array) { - obj.array[obj.index] = new_element; - } else { - obj.object[obj.key] = new_element; - } - }); - - let anim_svg = svg_container.cloneNode(true); - - anim_elements.map((anim_element, i) => { - parent.replaceChild(new_elements[i], anim_element); - elements[i][0] = new_elements[i]; - }); - - return anim_svg; -} - -$$.animateSVG = (element, props, dur, easing_type="linear") => { - let easing = { - ease: "0.25 0.1 0.25 1", - linear: "0 0 1 1", - // easein: "0.42 0 1 1", - easein: "0.1 0.8 0.2 1", - easeout: "0 0 0.58 1", - easeinout: "0.42 0 0.58 1" - } - - let anim_element = element.cloneNode(false); - let new_element = element.cloneNode(false); - - for(var attributeName in props) { - let animate_element = document.createElementNS("http://www.w3.org/2000/svg", "animate"); - - let current_value = element.getAttribute(attributeName); - let value = props[attributeName]; - - let anim_attr = { - attributeName: attributeName, - from: current_value, - to: value, - begin: "0s", - dur: dur/1000 + "s", - values: current_value + ";" + value, - keySplines: easing[easing_type], - keyTimes: "0;1", - calcMode: "spline" - } - - for (var i in anim_attr) { - animate_element.setAttribute(i, anim_attr[i]); - } - - anim_element.appendChild(animate_element); - new_element.setAttribute(attributeName, value); - } - - return [anim_element, new_element]; -} - -$$.bind = function(element, o) { - if (element) { - for (var event in o) { - var callback = o[event]; - - event.split(/\s+/).forEach(function (event) { - element.addEventListener(event, callback); - }); - } - } -}; - -$$.unbind = function(element, o) { - if (element) { - for (var event in o) { - var callback = o[event]; - - event.split(/\s+/).forEach(function(event) { - element.removeEventListener(event, callback); - }); - } - } -}; - -$$.fire = function(target, type, properties) { - var evt = document.createEvent("HTMLEvents"); - - evt.initEvent(type, true, true ); - - for (var j in properties) { - evt[j] = properties[j]; - } - - return target.dispatchEvent(evt); -}; diff --git a/frappe/public/js/frappe/ui/colors.js b/frappe/public/js/frappe/ui/colors.js index 35b76a52c6..659e547524 100644 --- a/frappe/public/js/frappe/ui/colors.js +++ b/frappe/public/js/frappe/ui/colors.js @@ -15,7 +15,9 @@ frappe.ui.color_map = { skyblue: ["#d2f1ff", "#a6e4ff", "#78d6ff", "#4f8ea8"], blue: ["#d2d2ff", "#a3a3ff", "#7575ff", "#4d4da8"], purple: ["#dac7ff", "#b592ff", "#8e58ff", "#5e3aa8"], - pink: ["#f8d4f8", "#f3aaf0", "#ec7dea", "#934f92"] + pink: ["#f8d4f8", "#f3aaf0", "#ec7dea", "#934f92"], + white: ["#d1d8dd", "#fafbfc", "#ffffff", ""], + black: ["#8D99A6", "#6c7680", "#36414c", "#212a33"] }; frappe.ui.color = { diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index 8163cf5ec8..a3ebf22712 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -86,6 +86,12 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({ click.apply(me, [values]); }); }, + disable_primary_action: function() { + this.get_primary_btn().addClass('disabled'); + }, + enable_primary_action: function() { + this.get_primary_btn().removeClass('disabled'); + }, make_head: function() { var me = this; this.set_title(this.title); @@ -97,9 +103,11 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({ // show it this.$wrapper.modal("show"); this.primary_action_fulfilled = false; + this.is_visible = true; }, hide: function(from_event) { this.$wrapper.modal("hide"); + this.is_visible = false; }, get_close_btn: function() { return this.$wrapper.find(".btn-modal-close"); diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index 3cbe1d9a60..00d582696a 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -41,29 +41,11 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ } }, - add_fields: function(fields) { - this.render(fields); - this.refresh_fields(fields); - }, - refresh_fields: function(fields) { - let fieldnames = fields.map((field) => { - if(field.fieldname) return field.fieldname; - }); - - this.fields_list.map(fieldobj => { - if(fieldnames.includes(fieldobj.df.fieldname)) { - fieldobj.refresh(); - if(fieldobj.df["default"]) { - fieldobj.set_input(fieldobj.df["default"]); - } - } - }); - }, first_button: false, focus_on_first_input: function() { if(this.no_focus) return; $.each(this.fields_list, function(i, f) { - if(!in_list(['Date', 'Datetime', 'Time'], f.df.fieldtype) && f.set_focus) { + if(!in_list(['Date', 'Datetime', 'Time', 'Check'], f.df.fieldtype) && f.set_focus) { f.set_focus(); return false; } diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js index 4927a17be1..2ff8b51d5a 100644 --- a/frappe/public/js/frappe/ui/filters/filters.js +++ b/frappe/public/js/frappe/ui/filters/filters.js @@ -140,7 +140,7 @@ frappe.ui.FilterList = Class.extend({ } if (i!==undefined) { // remove index - this.splice(i, 1); + this.filters.splice(i, 1); } }, diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index df64bc77f4..f786d76d7c 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -235,8 +235,7 @@ frappe.verify_password = function(callback) { } frappe.show_progress = function(title, count, total=100, description) { - if(frappe.cur_progress && frappe.cur_progress.title === title - && frappe.cur_progress.$wrapper.is(":visible")) { + if(frappe.cur_progress && frappe.cur_progress.title === title && frappe.cur_progress.is_visible) { var dialog = frappe.cur_progress; } else { var dialog = new frappe.ui.Dialog({ diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index 5fde62d898..bf969f1566 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -275,7 +275,7 @@ frappe.ui.Page = Class.extend({ return $('
    • ').appendTo(this.menu); }, - get_inner_group_button: function(label) { + get_or_add_inner_group_button: function(label) { var $group = this.inner_toolbar.find('.btn-group[data-label="'+label+'"]'); if(!$group.length) { $group = $('
      \ @@ -286,8 +286,12 @@ frappe.ui.Page = Class.extend({ return $group; }, + get_inner_group_button: function(label) { + return this.inner_toolbar.find('.btn-group[data-label="'+label+'"]'); + }, + set_inner_btn_group_as_primary: function(label) { - this.get_inner_group_button(label).find("button").removeClass("btn-default").addClass("btn-primary"); + this.get_or_add_inner_group_button(label).find("button").removeClass("btn-default").addClass("btn-primary"); }, btn_disable_enable: function(btn, response) { @@ -312,7 +316,7 @@ frappe.ui.Page = Class.extend({ me.btn_disable_enable(btn, response); }; if(group) { - var $group = this.get_inner_group_button(group); + var $group = this.get_or_add_inner_group_button(group); $(this.inner_toolbar).removeClass("hide"); return $('
    • '+label+'
    • ') .on('click', _action) @@ -324,6 +328,29 @@ frappe.ui.Page = Class.extend({ } }, + remove_inner_button: function(label, group) { + if (typeof label === 'string') { + label = [label]; + } + // translate + label = label.map(l => __(l)); + + if (group) { + var $group = this.get_inner_group_button(__(group)); + if($group.length) { + $group.find('.dropdown-menu li a') + .filter((i, btn) => label.includes($(btn).text())) + .remove(); + } + if ($group.find('.dropdown-menu li a').length === 0) $group.remove(); + } else { + + this.inner_toolbar.find('button') + .filter((i, btn) => label.includes($(btn).text())) + .remove(); + } + }, + clear_inner_toolbar: function() { this.inner_toolbar.empty().addClass("hide"); }, @@ -495,4 +522,4 @@ frappe.ui.Page = Class.extend({ this.wrapper.trigger('view-change'); }, -}); \ No newline at end of file +}); diff --git a/frappe/public/js/frappe/ui/slides.js b/frappe/public/js/frappe/ui/slides.js index 97fb95c0b6..e51a49cff4 100644 --- a/frappe/public/js/frappe/ui/slides.js +++ b/frappe/public/js/frappe/ui/slides.js @@ -194,6 +194,10 @@ frappe.ui.Slide = class Slide { return this.form.get_field(fieldname); } + get_value(fieldname) { + return this.form.get_value(fieldname); + } + destroy() { this.$body.remove(); } diff --git a/frappe/public/js/frappe/ui/tag_editor.js b/frappe/public/js/frappe/ui/tag_editor.js new file mode 100644 index 0000000000..3efab96aa4 --- /dev/null +++ b/frappe/public/js/frappe/ui/tag_editor.js @@ -0,0 +1,129 @@ +frappe.ui.TagEditor = Class.extend({ + init: function(opts) { + /* docs: + Arguments + + - parent + - user_tags + - doctype + - docname + */ + $.extend(this, opts); + + this.setup_tags(); + + if (!this.user_tags) { + this.user_tags = ""; + } + this.initialized = true; + this.refresh(this.user_tags); + }, + setup_tags: function() { + var me = this; + + // hidden form, does not have parent + if (!this.parent) { + return; + } + + this.wrapper = $('
      ').appendTo(this.parent); + if(!this.wrapper.length) return; + + this.tags = new frappe.ui.Tags({ + parent: this.wrapper, + placeholder: "Add a tag ...", + onTagAdd: (tag) => { + if(me.initialized && !me.refreshing) { + tag = toTitle(tag); + return frappe.call({ + method: 'frappe.desk.tags.add_tag', + args: me.get_args(tag), + callback: function(r) { + var user_tags = me.user_tags ? me.user_tags.split(",") : []; + user_tags.push(tag) + me.user_tags = user_tags.join(","); + me.on_change && me.on_change(me.user_tags); + } + }); + } + }, + onTagRemove: (tag) => { + if(!me.refreshing) { + return frappe.call({ + method: 'frappe.desk.tags.remove_tag', + args: me.get_args(tag), + callback: function(r) { + var user_tags = me.user_tags.split(","); + user_tags.splice(user_tags.indexOf(tag), 1); + me.user_tags = user_tags.join(","); + me.on_change && me.on_change(me.user_tags); + } + }); + } + } + }); + this.setup_awesomplete(); + this.setup_complete = true; + }, + setup_awesomplete: function() { + var me = this; + var $input = this.wrapper.find("input.tags-input"); + var input = $input.get(0); + this.awesomplete = new Awesomplete(input, { + minChars: 0, + maxItems: 99, + list: [] + }); + $input.on("awesomplete-open", function(e){ + $input.attr('state', 'open'); + }); + $input.on("awesomplete-close", function(e){ + $input.attr('state', 'closed'); + }); + $input.on("input", function(e) { + var value = e.target.value; + frappe.call({ + method:"frappe.desk.tags.get_tags", + args:{ + doctype: me.frm.doctype, + txt: value.toLowerCase(), + cat_tags: me.list_sidebar ? + JSON.stringify(me.list_sidebar.get_cat_tags()) : '[]' + }, + callback: function(r) { + me.awesomplete.list = r.message; + } + }); + }); + $input.on("focus", function(e) { + if($input.attr('state') != 'open') { + $input.trigger("input"); + } + }); + }, + get_args: function(tag) { + return { + tag: tag, + dt: this.frm.doctype, + dn: this.frm.docname, + } + }, + refresh: function(user_tags) { + var me = this; + if (!this.initialized || !this.setup_complete || this.refreshing) return; + + me.refreshing = true; + try { + me.tags.clearTags(); + if(user_tags) { + me.tags.addTags(user_tags.split(',')); + } + } catch(e) { + me.refreshing = false; + // wtf bug + setTimeout( function() { me.refresh(); }, 100); + } + me.refreshing = false; + + } +}) diff --git a/frappe/public/js/frappe/ui/tags.js b/frappe/public/js/frappe/ui/tags.js index f99da28ff7..e520d56978 100644 --- a/frappe/public/js/frappe/ui/tags.js +++ b/frappe/public/js/frappe/ui/tags.js @@ -1,131 +1,135 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt -frappe.ui.TagEditor = Class.extend({ - init: function(opts) { - /* docs: - Arguments - - - parent - - user_tags - - doctype - - docname - */ - $.extend(this, opts); - - this.setup_taggle(); - - if (!this.user_tags) { - this.user_tags = ""; - } - this.initialized = true; - this.refresh(this.user_tags); - }, - setup_taggle: function() { - var me = this; - - // hidden form, does not have parent - if (!this.parent) { - return; - } +frappe.ui.Tags = class { + constructor({ + parent, placeholder, tagsList, + onTagAdd, + onTagRemove, + onTagClick, + onChange + }) { + this.tagsList = tagsList || []; + this.onTagAdd = onTagAdd; + this.onTagRemove = onTagRemove; + this.onTagClick = onTagClick; + this.onChange = onChange; + + this.setup(parent, placeholder); + } + + setup(parent, placeholder) { + this.$wrapper = $(`
      `).appendTo(parent); + this.$ul = $(`
        `).appendTo(this.$wrapper); + this.$input = $(``); + + this.$inputWrapper = this.getListElement(this.$input); + this.$placeholder = this.getListElement($(`${placeholder}`)); + this.$inputWrapper.appendTo(this.$ul); + this.$placeholder.appendTo(this.$ul); - this.wrapper = $('
        ').appendTo(this.parent); - if(!this.wrapper.length) return; - var id = frappe.dom.set_unique_id(this.wrapper); - this.taggle = new Taggle(id, { - placeholder: __('Add a tag') + "...", - onTagAdd: function(e, tag) { - if(me.initialized && !me.refreshing) { - tag = toTitle(tag); - return frappe.call({ - method: 'frappe.desk.tags.add_tag', - args: me.get_args(tag), - callback: function(r) { - var user_tags = me.user_tags ? me.user_tags.split(",") : []; - user_tags.push(tag) - me.user_tags = user_tags.join(","); - me.on_change && me.on_change(me.user_tags); - } - }); - } - }, - onTagRemove: function(e, tag) { - if(!me.refreshing) { - return frappe.call({ - method: 'frappe.desk.tags.remove_tag', - args: me.get_args(tag), - callback: function(r) { - var user_tags = me.user_tags.split(","); - user_tags.splice(user_tags.indexOf(tag), 1); - me.user_tags = user_tags.join(","); - me.on_change && me.on_change(me.user_tags); - } - }); - } + this.deactivate(); + this.bind(); + this.boot(); + } + + bind() { + this.$input.keypress((e) => { + if(e.which == 13 || e.keyCode == 13) { + this.addTag(this.$input.val()); + this.$input.val(''); } }); - this.setup_awesomplete(); - this.setup_complete = true; - }, - setup_awesomplete: function() { - var me = this; - var $input = this.wrapper.find("input.taggle_input"); - var input = $input.get(0); - this.awesomplete = new Awesomplete(input, { - minChars: 0, - maxItems: 99, - list: [] + + this.$input.on('blur', () => { + this.deactivate(); }); - $input.on("awesomplete-open", function(e){ - $input.attr('state', 'open'); + + this.$placeholder.on('click', () => { + this.activate(); }); - $input.on("awesomplete-close", function(e){ - $input.attr('state', 'closed'); + } + + boot() { + this.addTags(this.tagsList); + } + + activate() { + this.$placeholder.hide(); + this.$inputWrapper.show(); + this.$input.focus(); + } + + deactivate() { + this.$inputWrapper.hide(); + this.$placeholder.show(); + } + + refresh() { + this.deactivate(); + this.activate(); + } + + addTag(label) { + if(label && !this.tagsList.includes(label)) { + let $tag = this.getTag(label); + this.getListElement($tag).insertBefore(this.$inputWrapper); + this.tagsList.push(label); + this.onTagAdd && this.onTagAdd(label); + + this.refresh(); + } + } + + removeTag(label) { + if(this.tagsList.includes(label)) { + let $tag = this.$ul.find(`.frappe-tag[data-tag-label="${label}"]`); + $tag.remove(); + this.tagsList = this.tagsList.filter(d => d !== label); + this.onTagRemove && this.onTagRemove(label); + } + } + + addTags(labels) { + labels.map(this.addTag.bind(this)); + } + + clearTags() { + this.$ul.find('.frappe-tag').remove(); + this.tagsList = []; + } + + getListElement($element, className) { + let $li = $(`
      • `); + $element.appendTo($li); + return $li; + } + + getTag(label) { + let $tag = $(`
        + +
        `); + + let $removeTag = $tag.find(".remove-tag"); + + $removeTag.on("click", () => { + this.removeTag($removeTag.attr('data-tag-label')); }); - $input.on("input", function(e) { - var value = e.target.value; - frappe.call({ - method:"frappe.desk.tags.get_tags", - args:{ - doctype: me.frm.doctype, - txt: value.toLowerCase(), - cat_tags: me.list_sidebar ? - JSON.stringify(me.list_sidebar.get_cat_tags()) : '[]' - }, - callback: function(r) { - me.awesomplete.list = r.message; - } + + if(this.onTagClick) { + let $toggle_tag = $tag.find(".toggle-tag"); + $toggle_tag.on("click", () => { + this.onTagClick($toggle_tag.attr('data-tag-label')); }); - }); - $input.on("focus", function(e) { - if($input.attr('state') != 'open') { - $input.trigger("input"); - } - }); - }, - get_args: function(tag) { - return { - tag: tag, - dt: this.frm.doctype, - dn: this.frm.docname, - } - }, - refresh: function(user_tags) { - var me = this; - if (!this.initialized || !this.setup_complete || this.refreshing) return; - - me.refreshing = true; - try { - me.taggle.removeAll(); - if(user_tags) { - me.taggle.add(user_tags.split(',')); - } - } catch(e) { - me.refreshing = false; - // wtf bug - setTimeout( function() { me.refresh(); }, 100); } - me.refreshing = false; + return $tag; } -}) +} diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index fed120d79d..f702994dcb 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -11,7 +11,7 @@