Merge branch 'develop' of https://github.com/frappe/frappe into web_feedback
This commit is contained in:
commit
20c5724b3f
155 changed files with 3208 additions and 3307 deletions
BIN
.github/frappe-framework-logo.png
vendored
BIN
.github/frappe-framework-logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 4.9 KiB |
4
.github/frappe-framework-logo.svg
vendored
Normal file
4
.github/frappe-framework-logo.svg
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 1082 125" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 56.284v67.508h26.904V79.368l47.902-.04V56.248L1 56.284zM82.994 1H1.034v23.043h81.96V1z" fill="#3C88F7"/>
|
||||
<path d="M216.735 75.269V53.233h-52.897V22.505h61.849V.469H138v122.509h25.838v-47.71h52.897zM285.477 59.096c2.645 0 5.29.404 7.731 1.415l3.052-26.281c-2.441-.607-4.883-1.01-7.934-1.01-11.8.201-19.125 6.67-25.635 22.439.61-3.639.814-7.48.814-11.321v-9.3h-23.193v87.94h25.227V72.64c5.697-12.13 15.056-13.544 19.938-13.544zM380.301 125c4.476 0 9.359-1.011 13.428-4.043l-2.442-17.184c-.813.404-1.627.607-2.645.607-2.644 0-4.272-2.426-4.272-6.672v-32.75c0-23.652-16.276-31.739-39.266-31.739-23.193 0-38.655 10.715-41.3 30.728l22.583 1.213c1.22-8.49 7.12-12.533 17.09-12.533 9.358 0 15.869 4.65 15.869 13.949v5.458h-16.683c-26.245 0-41.097 8.895-41.097 27.898 0 18.397 14.648 25.068 31.128 25.068 15.462 0 24.007-6.671 28.89-15.364C363.211 121.563 372.163 125 380.301 125zm-20.752-39.22c0 14.759-10.579 21.228-19.735 21.228-7.324 0-12.41-3.033-12.41-9.704 0-8.086 6.917-11.523 18.107-11.523h14.038zM449.522 55.255c9.562 0 15.055 5.862 15.055 19.003v48.72h25.228V69.001c3.458-8.895 9.358-13.746 16.683-13.746 10.172 0 15.055 5.862 15.055 19.003v48.72h25.228v-54.38c0-24.462-11.8-35.379-32.145-35.379-11.8 0-22.38 5.054-29.297 15.97-4.476-11.118-13.021-15.97-26.856-15.97-13.02 0-22.379 5.66-28.076 17.184.204-2.022.204-3.841.204-5.863V35.04h-23.193v87.939h25.227V69.406c3.459-9.097 8.952-14.151 16.887-14.151zM606.579 125c22.583 0 39.062-8.491 44.148-29.111l-24.21-2.224c-2.442 7.48-8.749 11.725-18.311 11.725-12.614 0-19.531-7.277-20.548-21.429l64.087-.202c.203-2.224.203-4.245.203-6.469 0-28.505-15.462-44.07-43.131-44.07-29.297 0-47.201 18.8-47.201 46.496 0 28.909 17.09 45.284 44.963 45.284zm1.831-72.98c10.783 0 17.09 6.065 17.903 17.184h-37.841c2.441-11.928 9.765-17.184 19.938-17.184zM751.095 122.978h34.18l24.414-87.94H782.63l-15.056 63.479h-1.831l-17.089-63.478h-28.89l-17.497 63.478h-2.035l-15.055-63.478h-27.669l24.617 87.939h34.383l16.48-61.456h1.831l16.276 61.456zM814.935 79.918c0 28.303 16.276 45.082 45.98 45.082 31.535 0 48.828-17.992 48.828-46.093 0-27.898-16.073-45.688-45.573-45.688-31.738 0-49.235 18.599-49.235 46.7zm26.652-.606c0-16.173 8.545-26.079 21.566-26.079 12.614 0 19.938 9.906 19.938 26.079 0 15.768-8.341 25.876-20.955 25.876-12.818 0-20.549-10.108-20.549-25.876zM971.128 59.096c2.645 0 5.289.404 7.731 1.415l3.052-26.281c-2.442-.607-4.883-1.01-7.935-1.01-11.8.201-19.124 6.67-25.635 22.439.611-3.639.814-7.48.814-11.321v-9.3h-23.193v87.94h25.228V72.64c5.696-12.13 15.055-13.544 19.938-13.544zM1070.4 125c3.46 0 7.33-.404 10.79-1.819l-1.83-21.227c-.82.202-1.63.404-3.26.404-4.27 0-7.12-2.224-11.39-7.682l-16.48-21.833L1082 35.039h-27.67l-34.18 36.793h-.61V1.5h-25.227v121.478h25.227v-20.62l10.78-12.534 14.45 19.61c8.95 12.938 15.87 15.566 25.63 15.566z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -4,8 +4,7 @@ pull_request_rules:
|
|||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk - package.json (frappe)
|
||||
- status-success=security/snyk - requirements.txt (frappe)
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=don't-merge
|
||||
- label!=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
|
|
@ -17,8 +16,7 @@ pull_request_rules:
|
|||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk - package.json (frappe)
|
||||
- status-success=security/snyk - requirements.txt (frappe)
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=don't-merge
|
||||
- label=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<div align="center">
|
||||
<img src=".github/frappe-framework-logo.png" height="150">
|
||||
<h1>
|
||||
<br>
|
||||
<a href="https://frappeframework.com">
|
||||
frappe
|
||||
<img src=".github/frappe-framework-logo.svg" height="50">
|
||||
</a>
|
||||
</h1>
|
||||
<h3>
|
||||
a web framework with <a href="https://www.youtube.com/watch?v=LOjk3m0wTwg">"batteries included"
|
||||
a web framework with <a href="https://www.youtube.com/watch?v=LOjk3m0wTwg">"batteries included"</a>
|
||||
</h3>
|
||||
<h5>
|
||||
it's pronounced - <em>fra-pay</em>
|
||||
|
|
|
|||
|
|
@ -490,7 +490,8 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
|
|||
message = content or message
|
||||
|
||||
if as_markdown:
|
||||
message = frappe.utils.md_to_html(message)
|
||||
from frappe.utils import md_to_html
|
||||
message = md_to_html(message)
|
||||
|
||||
if not delayed:
|
||||
now = True
|
||||
|
|
|
|||
|
|
@ -333,12 +333,20 @@ class CookieManager:
|
|||
# sid expires in 3 days
|
||||
expires = datetime.datetime.now() + datetime.timedelta(days=3)
|
||||
if frappe.session.sid:
|
||||
self.cookies["sid"] = {"value": frappe.session.sid, "expires": expires}
|
||||
self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True)
|
||||
if frappe.session.session_country:
|
||||
self.cookies["country"] = {"value": frappe.session.get("session_country")}
|
||||
self.set_cookie("country", frappe.session.session_country)
|
||||
|
||||
def set_cookie(self, key, value, expires=None):
|
||||
self.cookies[key] = {"value": value, "expires": expires}
|
||||
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
|
||||
if not secure:
|
||||
secure = frappe.local.request.scheme == "https"
|
||||
self.cookies[key] = {
|
||||
"value": value,
|
||||
"expires": expires,
|
||||
"secure": secure,
|
||||
"httponly": httponly,
|
||||
"samesite": samesite
|
||||
}
|
||||
|
||||
def delete_cookie(self, to_delete):
|
||||
if not isinstance(to_delete, (list, tuple)):
|
||||
|
|
@ -349,7 +357,10 @@ class CookieManager:
|
|||
def flush_cookies(self, response):
|
||||
for key, opts in self.cookies.items():
|
||||
response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')),
|
||||
expires=opts.get("expires"))
|
||||
expires=opts.get("expires"),
|
||||
secure=opts.get("secure"),
|
||||
httponly=opts.get("httponly"),
|
||||
samesite=opts.get("samesite"))
|
||||
|
||||
# expires yesterday!
|
||||
expires = datetime.datetime.now() + datetime.timedelta(days=-1)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class AssignmentRule(Document):
|
|||
def on_update(self): # pylint: disable=no-self-use
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
|
||||
|
||||
def after_rename(self): # pylint: disable=no-self-use
|
||||
def after_rename(self, old, new, merge): # pylint: disable=no-self-use
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
|
||||
|
||||
def apply_unassign(self, doc, assignments):
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ class AutoRepeat(Document):
|
|||
|
||||
def make_new_document(self):
|
||||
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
|
||||
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
|
||||
new_doc = frappe.copy_doc(reference_doc)
|
||||
self.update_doc(new_doc, reference_doc)
|
||||
new_doc.insert(ignore_permissions = True)
|
||||
|
||||
|
|
@ -372,7 +372,8 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e
|
|||
doc.save()
|
||||
return doc
|
||||
|
||||
#method for reference_doctype filter
|
||||
# method for reference_doctype filter
|
||||
@frappe.whitelist()
|
||||
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
||||
res = frappe.db.get_all('Property Setter', {
|
||||
'property': 'allow_auto_repeat',
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ def load_desktop_data(bootinfo):
|
|||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
from frappe.desk.desktop import get_desk_sidebar_items
|
||||
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items(True)
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False)
|
||||
bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ global_cache_keys = ("app_hooks", "installed_apps",
|
|||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
|
||||
"defaults", "user_permissions", "home_page", "linked_with",
|
||||
"desktop_icons", 'portal_menu_items', 'user_perm_can_read',
|
||||
"has_role:Page", "has_role:Report")
|
||||
"has_role:Page", "has_role:Report", "desk_sidebar_items")
|
||||
|
||||
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
|
||||
"linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map', 'data_import_column_header_map')
|
||||
|
|
|
|||
|
|
@ -108,12 +108,14 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
@click.option('--install-app', multiple=True, help='Install app after installation')
|
||||
@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file')
|
||||
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
|
||||
@click.option('--force', is_flag=True, default=False, help='Use a bit of force to get the job done')
|
||||
@pass_context
|
||||
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
|
||||
"Restore site database from an sql file"
|
||||
from frappe.installer import extract_sql_gzip, extract_tar_files
|
||||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
|
||||
from frappe.installer import extract_sql_gzip, extract_tar_files, is_downgrade
|
||||
force = context.force or force
|
||||
|
||||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
|
||||
if not os.path.exists(sql_file_path):
|
||||
base_path = '..'
|
||||
sql_file_path = os.path.join(base_path, sql_file_path)
|
||||
|
|
@ -125,7 +127,6 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
else:
|
||||
base_path = '.'
|
||||
|
||||
|
||||
if sql_file_path.endswith('sql.gz'):
|
||||
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
|
||||
else:
|
||||
|
|
@ -133,10 +134,16 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
# dont allow downgrading to older versions of frappe without force
|
||||
if not force and is_downgrade(decompressed_file_name, verbose=True):
|
||||
warn_message = "This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?"
|
||||
click.confirm(warn_message, abort=True)
|
||||
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True)
|
||||
force=True, db_type=frappe.conf.db_type)
|
||||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
|
|
@ -194,16 +201,31 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro
|
|||
def install_app(context, apps):
|
||||
"Install a new app to site, supports multiple apps"
|
||||
from frappe.installer import install_app as _install_app
|
||||
exit_code = 0
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
try:
|
||||
for app in apps:
|
||||
|
||||
for app in apps:
|
||||
try:
|
||||
_install_app(app, verbose=context.verbose)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
except frappe.IncompatibleApp as err:
|
||||
err_msg = ":\n{}".format(err) if str(err) else ""
|
||||
print("App {} is Incompatible with Site {}{}".format(app, site, err_msg))
|
||||
exit_code = 1
|
||||
except Exception as err:
|
||||
err_msg = ":\n{}".format(err if str(err) else frappe.get_traceback())
|
||||
print("An error occurred while installing {}{}".format(app, err_msg))
|
||||
exit_code = 1
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@click.command('list-apps')
|
||||
@pass_context
|
||||
|
|
@ -415,15 +437,16 @@ def remove_from_installed_apps(context, app):
|
|||
@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True)
|
||||
@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False)
|
||||
@click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False)
|
||||
@click.option('--force', help='Force remove app from site', is_flag=True, default=False)
|
||||
@pass_context
|
||||
def uninstall(context, app, dry_run=False, yes=False, no_backup=False):
|
||||
def uninstall(context, app, dry_run, yes, no_backup, force):
|
||||
"Remove app and linked modules from site"
|
||||
from frappe.installer import remove_app
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
remove_app(app, dry_run, yes, no_backup)
|
||||
remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
|
|
@ -608,6 +631,29 @@ def stop_recording(context):
|
|||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('ngrok')
|
||||
@pass_context
|
||||
def start_ngrok(context):
|
||||
from pyngrok import ngrok
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
port = frappe.conf.http_port or frappe.conf.webserver_port
|
||||
public_url = ngrok.connect(port=port, options={
|
||||
'host_header': site
|
||||
})
|
||||
print(f'Public URL: {public_url}')
|
||||
print('Inspect logs at http://localhost:4040')
|
||||
|
||||
ngrok_process = ngrok.get_ngrok_process()
|
||||
try:
|
||||
# Block until CTRL-C or some other terminating event
|
||||
ngrok_process.proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("Shutting down server...")
|
||||
frappe.destroy()
|
||||
ngrok.kill()
|
||||
|
||||
commands = [
|
||||
add_system_manager,
|
||||
|
|
@ -633,5 +679,6 @@ commands = [
|
|||
browse,
|
||||
start_recording,
|
||||
stop_recording,
|
||||
add_to_hosts
|
||||
add_to_hosts,
|
||||
start_ngrok
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ def get_data():
|
|||
"description": _("Language, Date and Time settings"),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Global Defaults",
|
||||
"label": _("Global Defaults"),
|
||||
"description": _("Company, Fiscal Year and Currency defaults"),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Error Log",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
{
|
||||
"hidden": 0,
|
||||
"label": "Core",
|
||||
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
|
||||
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company, Fiscal Year and Currency defaults\",\n \"hide_count\": true,\n \"label\": \"Global Defaults\",\n \"name\": \"Global Defaults\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
|
|
@ -39,10 +39,11 @@
|
|||
"docstatus": 0,
|
||||
"doctype": "Desk Page",
|
||||
"extends_another_page": 0,
|
||||
"hide_custom": 0,
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Settings",
|
||||
"modified": "2020-04-01 11:24:40.636747",
|
||||
"modified": "2020-07-14 10:09:09.520557",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Settings",
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
|
|||
:param print_html: Send given value as HTML attachment.
|
||||
:param print_format: Attach print format of parent document."""
|
||||
|
||||
view_link = frappe.utils.cint(frappe.db.get_value("Print Settings", "Print Settings", "attach_view_link"))
|
||||
view_link = frappe.utils.cint(frappe.db.get_value("System Settings", "System Settings", "attach_view_link"))
|
||||
|
||||
if print_format and view_link:
|
||||
doc.content += get_attach_link(doc, print_format)
|
||||
|
|
@ -236,7 +236,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
|
|||
|
||||
if doc.sender:
|
||||
# combine for sending to get the format 'Jane <jane@example.com>'
|
||||
doc.sender = formataddr([doc.sender_full_name, doc.sender])
|
||||
doc.sender = get_formatted_email(doc.sender_full_name, mail=doc.sender)
|
||||
|
||||
doc.attachments = []
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@ frappe.ui.form.on('Data Import', {
|
|||
},
|
||||
|
||||
update_primary_action(frm) {
|
||||
if (frm.is_dirty()) {
|
||||
frm.enable_save();
|
||||
return;
|
||||
}
|
||||
frm.disable_save();
|
||||
if (frm.doc.status !== 'Success') {
|
||||
if (!frm.is_new() && (frm.has_import_file())) {
|
||||
|
|
@ -199,20 +203,12 @@ frappe.ui.form.on('Data Import', {
|
|||
},
|
||||
|
||||
download_template(frm) {
|
||||
if (
|
||||
frm.data_exporter &&
|
||||
frm.data_exporter.doctype === frm.doc.reference_doctype
|
||||
) {
|
||||
frm.data_exporter.exporting_for = frm.doc.import_type;
|
||||
frm.data_exporter.dialog.show();
|
||||
} else {
|
||||
frappe.require('/assets/js/data_import_tools.min.js', () => {
|
||||
frm.data_exporter = new frappe.data_import.DataExporter(
|
||||
frm.doc.reference_doctype,
|
||||
frm.doc.import_type
|
||||
);
|
||||
});
|
||||
}
|
||||
frappe.require('/assets/js/data_import_tools.min.js', () => {
|
||||
frm.data_exporter = new frappe.data_import.DataExporter(
|
||||
frm.doc.reference_doctype,
|
||||
frm.doc.import_type
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
reference_doctype(frm) {
|
||||
|
|
@ -301,8 +297,8 @@ frappe.ui.form.on('Data Import', {
|
|||
events: {
|
||||
remap_column(changed_map) {
|
||||
let template_options = JSON.parse(frm.doc.template_options || '{}');
|
||||
template_options.remap_column = template_options.remap_column || {};
|
||||
Object.assign(template_options.remap_column, changed_map);
|
||||
template_options.column_to_field_map = template_options.column_to_field_map || {};
|
||||
Object.assign(template_options.column_to_field_map, changed_map);
|
||||
frm.set_value('template_options', JSON.stringify(template_options));
|
||||
frm.save().then(() => frm.trigger('import_file'));
|
||||
}
|
||||
|
|
@ -435,10 +431,10 @@ frappe.ui.form.on('Data Import', {
|
|||
.join('');
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
<button class="btn btn-default btn-xs margin-top" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}">
|
||||
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
|
||||
${__('Show Traceback')}
|
||||
</button>
|
||||
<div class="collapse margin-top" id="${id}">
|
||||
<div class="collapse" id="${id}" style="margin-top: 15px;">
|
||||
<div class="well">
|
||||
<pre>${log.exception}</pre>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@
|
|||
{
|
||||
"fieldname": "import_warnings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Warnings"
|
||||
"label": "Import File Errors and Warnings"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings",
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
"label": "Import Warnings"
|
||||
},
|
||||
{
|
||||
"depends_on": "reference_doctype",
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "download_template",
|
||||
"fieldtype": "Button",
|
||||
"label": "Download Template"
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
"label": "Import from Google Sheets"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.google_sheets_url",
|
||||
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved",
|
||||
"fieldname": "refresh_google_sheet",
|
||||
"fieldtype": "Button",
|
||||
"label": "Refresh Google Sheet"
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-18 16:05:54.211034",
|
||||
"modified": "2020-06-24 14:33:03.173876",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import (
|
|||
read_xls_file_from_attached_file,
|
||||
)
|
||||
from frappe.model import no_value_fields, table_fields as table_fieldtypes
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
|
||||
INVALID_VALUES = ("", None)
|
||||
MAX_ROWS_IN_PREVIEW = 10
|
||||
|
|
@ -58,6 +59,7 @@ class Importer:
|
|||
frappe.flags.in_import = True
|
||||
frappe.flags.mute_emails = self.data_import.mute_emails
|
||||
|
||||
self.data_import.db_set("status", "Pending")
|
||||
self.data_import.db_set("template_warnings", "")
|
||||
|
||||
def import_data(self):
|
||||
|
|
@ -216,14 +218,22 @@ class Importer:
|
|||
def update_record(self, doc):
|
||||
id_field = get_id_field(self.doctype)
|
||||
existing_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname))
|
||||
existing_doc.flags.updater_reference = {
|
||||
"doctype": self.data_import.doctype,
|
||||
"docname": self.data_import.name,
|
||||
"label": _("via Data Import"),
|
||||
}
|
||||
existing_doc.update(doc)
|
||||
existing_doc.save()
|
||||
return existing_doc
|
||||
|
||||
updated_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname))
|
||||
updated_doc.update(doc)
|
||||
|
||||
if get_diff(existing_doc, updated_doc):
|
||||
# update doc if there are changes
|
||||
updated_doc.flags.updater_reference = {
|
||||
"doctype": self.data_import.doctype,
|
||||
"docname": self.data_import.name,
|
||||
"label": _("via Data Import"),
|
||||
}
|
||||
updated_doc.save()
|
||||
return updated_doc
|
||||
else:
|
||||
# throw if no changes
|
||||
frappe.throw('No changes to update')
|
||||
|
||||
def get_eta(self, current, total, processing_time):
|
||||
self.last_eta = getattr(self, "last_eta", 0)
|
||||
|
|
@ -306,8 +316,9 @@ class ImportFile:
|
|||
)
|
||||
self.column_to_field_map = self.template_options.column_to_field_map
|
||||
self.import_type = import_type
|
||||
self.warnings = []
|
||||
|
||||
self.file_doc = self.file_path = None
|
||||
self.file_doc = self.file_path = self.google_sheets_url = None
|
||||
if isinstance(file, frappe.string_types):
|
||||
if frappe.db.exists("File", {"file_url": file}):
|
||||
self.file_doc = frappe.get_doc("File", {"file_url": file})
|
||||
|
|
@ -430,9 +441,8 @@ class ImportFile:
|
|||
|
||||
# if there are child doctypes, find the subsequent rows
|
||||
if len(doctypes) > 1:
|
||||
# subsequent rows either dont have any parent value set
|
||||
# or have the same value as the parent row
|
||||
# we include a row if either of conditions match
|
||||
# subsequent rows that have blank values in parent columns
|
||||
# are considered as child rows
|
||||
parent_column_indexes = self.header.get_column_indexes(self.doctype)
|
||||
parent_row_values = first_row.get_values(parent_column_indexes)
|
||||
|
||||
|
|
@ -443,11 +453,8 @@ class ImportFile:
|
|||
if all([v in INVALID_VALUES for v in row_values]):
|
||||
rows.append(row)
|
||||
continue
|
||||
# if the row has same values as parent row, it's a child row doc
|
||||
if row_values == parent_row_values:
|
||||
rows.append(row)
|
||||
continue
|
||||
# if any of those conditions dont match, it's the next doc
|
||||
# if we encounter a row which has values in parent columns,
|
||||
# then it is the next doc
|
||||
break
|
||||
|
||||
parent_doc = None
|
||||
|
|
@ -462,38 +469,46 @@ class ImportFile:
|
|||
parent_doc[table_df.fieldname].append(child_doc)
|
||||
|
||||
doc = parent_doc
|
||||
# check if there is atleast one row for mandatory table fields
|
||||
meta = frappe.get_meta(self.doctype)
|
||||
mandatory_table_fields = [
|
||||
df
|
||||
for df in meta.fields
|
||||
if df.fieldtype in table_fieldtypes
|
||||
and df.reqd
|
||||
and len(doc.get(df.fieldname, [])) == 0
|
||||
]
|
||||
if len(mandatory_table_fields) == 1:
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": first_row.row_number,
|
||||
"message": _("There should be atleast one row for {0} table").format(
|
||||
mandatory_table_fields[0].label
|
||||
),
|
||||
}
|
||||
)
|
||||
elif mandatory_table_fields:
|
||||
fields_string = ", ".join([df.label for df in mandatory_table_fields])
|
||||
message = _("There should be atleast one row for the following tables: {0}").format(
|
||||
fields_string
|
||||
)
|
||||
self.warnings.append({"row": first_row.row_number, "message": message})
|
||||
|
||||
if self.import_type == INSERT:
|
||||
# check if there is atleast one row for mandatory table fields
|
||||
meta = frappe.get_meta(self.doctype)
|
||||
mandatory_table_fields = [
|
||||
df
|
||||
for df in meta.fields
|
||||
if df.fieldtype in table_fieldtypes
|
||||
and df.reqd
|
||||
and len(doc.get(df.fieldname, [])) == 0
|
||||
]
|
||||
if len(mandatory_table_fields) == 1:
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": first_row.row_number,
|
||||
"message": _("There should be atleast one row for {0} table").format(
|
||||
frappe.bold(mandatory_table_fields[0].label)
|
||||
),
|
||||
}
|
||||
)
|
||||
elif mandatory_table_fields:
|
||||
fields_string = ", ".join([df.label for df in mandatory_table_fields])
|
||||
message = _("There should be atleast one row for the following tables: {0}").format(
|
||||
fields_string
|
||||
)
|
||||
self.warnings.append({"row": first_row.row_number, "message": message})
|
||||
|
||||
return doc, rows, data[len(rows) :]
|
||||
|
||||
def get_warnings(self):
|
||||
warnings = []
|
||||
|
||||
# ImportFile warnings
|
||||
warnings += self.warnings
|
||||
|
||||
# Column warnings
|
||||
for col in self.header.columns:
|
||||
warnings += col.warnings
|
||||
|
||||
# Row warnings
|
||||
for row in self.data:
|
||||
warnings += row.warnings
|
||||
|
||||
|
|
@ -600,14 +615,14 @@ class Row:
|
|||
def validate_value(self, value, col):
|
||||
df = col.df
|
||||
if df.fieldtype == "Select":
|
||||
select_options = df.get_select_options()
|
||||
select_options = [d for d in (df.options or '').split('\n') if d]
|
||||
if select_options and value not in select_options:
|
||||
options_string = ", ".join([frappe.bold(d) for d in select_options])
|
||||
msg = _("Value must be one of {0}").format(options_string)
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"field": df.as_dict(convert_dates_to_str=True),
|
||||
"field": df_as_json(df),
|
||||
"message": msg,
|
||||
}
|
||||
)
|
||||
|
|
@ -622,7 +637,7 @@ class Row:
|
|||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"field": df.as_dict(convert_dates_to_str=True),
|
||||
"field": df_as_json(df),
|
||||
"message": msg,
|
||||
}
|
||||
)
|
||||
|
|
@ -635,7 +650,7 @@ class Row:
|
|||
{
|
||||
"row": self.row_number,
|
||||
"col": col.column_number,
|
||||
"field": df.as_dict(convert_dates_to_str=True),
|
||||
"field": df_as_json(df),
|
||||
"message": _("Value {0} must in {1} format").format(
|
||||
frappe.bold(value), frappe.bold(get_user_format(col.date_format))
|
||||
),
|
||||
|
|
@ -646,7 +661,7 @@ class Row:
|
|||
return value
|
||||
|
||||
def link_exists(self, value, df):
|
||||
key = df.options + "::" + value
|
||||
key = df.options + "::" + cstr(value)
|
||||
if Row.link_values_exist_map.get(key) is None:
|
||||
Row.link_values_exist_map[key] = frappe.db.exists(df.options, value)
|
||||
return Row.link_values_exist_map.get(key)
|
||||
|
|
@ -674,6 +689,9 @@ class Row:
|
|||
return value
|
||||
|
||||
def get_date(self, value, column):
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
|
||||
date_format = column.date_format
|
||||
if date_format:
|
||||
try:
|
||||
|
|
@ -755,19 +773,21 @@ class Row:
|
|||
|
||||
|
||||
class Header(Row):
|
||||
def __init__(self, index, row, doctype, raw_data, column_to_field_map):
|
||||
def __init__(self, index, row, doctype, raw_data, column_to_field_map=None):
|
||||
self.index = index
|
||||
self.row_number = index + 1
|
||||
self.data = row
|
||||
self.doctype = doctype
|
||||
column_to_field_map = column_to_field_map or frappe._dict()
|
||||
|
||||
self.seen = []
|
||||
self.columns = []
|
||||
|
||||
for j, header in enumerate(row):
|
||||
column_values = [get_item_at_index(r, j) for r in raw_data]
|
||||
map_to_field = column_to_field_map.get(str(j))
|
||||
column = Column(
|
||||
j, header, self.doctype, column_values, column_to_field_map.get(header), self.seen
|
||||
j, header, self.doctype, column_values, map_to_field, self.seen
|
||||
)
|
||||
self.seen.append(header)
|
||||
self.columns.append(column)
|
||||
|
|
@ -824,7 +844,7 @@ class Column:
|
|||
|
||||
self.meta = frappe.get_meta(doctype)
|
||||
self.parse()
|
||||
self.parse_date_format()
|
||||
self.validate_values()
|
||||
|
||||
def parse(self):
|
||||
header_title = self.header_title
|
||||
|
|
@ -897,10 +917,6 @@ class Column:
|
|||
self.df = df
|
||||
self.skip_import = skip_import
|
||||
|
||||
def parse_date_format(self):
|
||||
if self.df and self.df.fieldtype in ("Date", "Time", "Datetime"):
|
||||
self.date_format = self.guess_date_format_for_column()
|
||||
|
||||
def guess_date_format_for_column(self):
|
||||
""" Guesses date format for a column by parsing all the values in the column,
|
||||
getting the date format and then returning the one which has the maximum frequency
|
||||
|
|
@ -935,6 +951,33 @@ class Column:
|
|||
|
||||
return max_occurred_date_format
|
||||
|
||||
def validate_values(self):
|
||||
if not self.df:
|
||||
return
|
||||
|
||||
if self.df.fieldtype == 'Link':
|
||||
# find all values that dont exist
|
||||
values = list(set([cstr(v) for v in self.column_values[1:] if v]))
|
||||
exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})]
|
||||
not_exists = list(set(values) - set(exists))
|
||||
if not_exists:
|
||||
missing_values = ', '.join(not_exists)
|
||||
self.warnings.append({
|
||||
'col': self.column_number,
|
||||
'message': "The following values do not exist for {}: {}".format(self.df.options, missing_values),
|
||||
'type': 'warning'
|
||||
})
|
||||
elif self.df.fieldtype in ("Date", "Time", "Datetime"):
|
||||
# guess date format
|
||||
self.date_format = self.guess_date_format_for_column()
|
||||
if not self.date_format:
|
||||
self.date_format = '%Y-%m-%d'
|
||||
self.warnings.append({
|
||||
'col': self.column_number,
|
||||
'message': _("Date format could not determined from the values in this column. Defaulting to yyyy-mm-dd."),
|
||||
'type': 'info'
|
||||
})
|
||||
|
||||
def as_dict(self):
|
||||
d = frappe._dict()
|
||||
d.index = self.index
|
||||
|
|
@ -944,6 +987,9 @@ class Column:
|
|||
d.map_to_field = self.map_to_field
|
||||
d.date_format = self.date_format
|
||||
d.df = self.df
|
||||
if hasattr(self.df, 'is_child_table_field'):
|
||||
d.is_child_table_field = self.df.is_child_table_field
|
||||
d.child_table_df = self.df.child_table_df
|
||||
d.skip_import = self.skip_import
|
||||
d.warnings = self.warnings
|
||||
return d
|
||||
|
|
@ -1021,6 +1067,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
# other fields
|
||||
fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields
|
||||
for df in fields:
|
||||
label = (df.label or '').strip()
|
||||
fieldtype = df.fieldtype or "Data"
|
||||
parent = df.parent or parent_doctype
|
||||
if fieldtype not in no_value_fields:
|
||||
|
|
@ -1029,12 +1076,12 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
# Label
|
||||
# label
|
||||
# Label (label)
|
||||
if not out.get(df.label):
|
||||
if not out.get(label):
|
||||
# if Label is already set, don't set it again
|
||||
# in case of duplicate column headers
|
||||
out[df.label] = df
|
||||
out[label] = df
|
||||
out[df.fieldname] = df
|
||||
label_with_fieldname = "{0} ({1})".format(df.label, df.fieldname)
|
||||
label_with_fieldname = "{0} ({1})".format(label, df.fieldname)
|
||||
out[label_with_fieldname] = df
|
||||
else:
|
||||
# in case there are multiple table fields with the same doctype
|
||||
|
|
@ -1045,7 +1092,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
"fields", {"fieldtype": ["in", table_fieldtypes], "options": parent}
|
||||
)
|
||||
for table_field in table_fields:
|
||||
by_label = "{0} ({1})".format(df.label, table_field.label)
|
||||
by_label = "{0} ({1})".format(label, table_field.label)
|
||||
by_fieldname = "{0}.{1}".format(table_field.fieldname, df.fieldname)
|
||||
|
||||
# create a new df object to avoid mutation problems
|
||||
|
|
@ -1113,3 +1160,13 @@ def get_user_format(date_format):
|
|||
.replace("%m", "mm")
|
||||
.replace("%d", "dd")
|
||||
)
|
||||
|
||||
def df_as_json(df):
|
||||
return {
|
||||
'fieldname': df.fieldname,
|
||||
'fieldtype': df.fieldtype,
|
||||
'label': df.label,
|
||||
'options': df.options,
|
||||
'parent': df.parent,
|
||||
'default': df.default
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2012-12-12 11:19:22",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -63,7 +64,8 @@
|
|||
"fieldname": "is_home_folder",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Home Folder"
|
||||
"label": "Is Home Folder",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -172,7 +174,8 @@
|
|||
],
|
||||
"icon": "fa fa-file",
|
||||
"idx": 1,
|
||||
"modified": "2019-08-30 19:46:20.796453",
|
||||
"links": [],
|
||||
"modified": "2020-06-28 12:21:30.772386",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "File",
|
||||
|
|
|
|||
|
|
@ -100,26 +100,26 @@ class File(Document):
|
|||
self.validate_file()
|
||||
self.generate_content_hash()
|
||||
|
||||
self.validate_url()
|
||||
|
||||
if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}):
|
||||
old_file_url = self.file_url
|
||||
if not self.is_folder and (self.is_private != self.db_get('is_private')):
|
||||
private_files = frappe.get_site_path('private', 'files')
|
||||
public_files = frappe.get_site_path('public', 'files')
|
||||
|
||||
file_name = self.file_url.split('/')[-1]
|
||||
if not self.is_private:
|
||||
shutil.move(os.path.join(private_files, self.file_name),
|
||||
os.path.join(public_files, self.file_name))
|
||||
shutil.move(os.path.join(private_files, file_name),
|
||||
os.path.join(public_files, file_name))
|
||||
|
||||
self.file_url = "/files/{0}".format(self.file_name)
|
||||
self.file_url = "/files/{0}".format(file_name)
|
||||
|
||||
else:
|
||||
shutil.move(os.path.join(public_files, self.file_name),
|
||||
os.path.join(private_files, self.file_name))
|
||||
shutil.move(os.path.join(public_files, file_name),
|
||||
os.path.join(private_files, file_name))
|
||||
|
||||
self.file_url = "/private/files/{0}".format(self.file_name)
|
||||
self.file_url = "/private/files/{0}".format(file_name)
|
||||
|
||||
update_existing_file_docs(self)
|
||||
|
||||
# update documents image url with new file url
|
||||
if self.attached_to_doctype and self.attached_to_name:
|
||||
|
|
@ -135,6 +135,8 @@ class File(Document):
|
|||
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
|
||||
self.attached_to_field, self.file_url)
|
||||
|
||||
self.validate_url()
|
||||
|
||||
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
|
||||
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
|
||||
|
||||
|
|
@ -182,13 +184,7 @@ class File(Document):
|
|||
if duplicate_file:
|
||||
duplicate_file_doc = frappe.get_cached_doc('File', duplicate_file.name)
|
||||
if duplicate_file_doc.exists_on_disk():
|
||||
# if it is attached to a document then throw FileAlreadyAttachedException
|
||||
if self.attached_to_doctype and self.attached_to_name:
|
||||
self.duplicate_entry = duplicate_file.name
|
||||
frappe.throw(_("Same file has already been attached to the record"),
|
||||
frappe.FileAlreadyAttachedException)
|
||||
# else just use the url, to avoid uploading a duplicate
|
||||
else:
|
||||
# just use the url, to avoid uploading a duplicate
|
||||
self.file_url = duplicate_file.file_url
|
||||
|
||||
def set_file_name(self):
|
||||
|
|
@ -909,3 +905,20 @@ def get_files_in_folder(folder):
|
|||
{ 'folder': folder },
|
||||
['name', 'file_name', 'file_url', 'is_folder', 'modified']
|
||||
)
|
||||
|
||||
def update_existing_file_docs(doc):
|
||||
# Update is private and file url of all file docs that point to the same file
|
||||
frappe.db.sql("""
|
||||
UPDATE `tabFile`
|
||||
SET
|
||||
file_url = %(file_url)s,
|
||||
is_private = %(is_private)s
|
||||
WHERE
|
||||
content_hash = %(content_hash)s
|
||||
and name != %(file_name)s
|
||||
""", dict(
|
||||
file_url=doc.file_url,
|
||||
is_private=doc.is_private,
|
||||
content_hash=doc.content_hash,
|
||||
file_name=doc.name
|
||||
))
|
||||
|
|
|
|||
|
|
@ -294,4 +294,37 @@ class TestFile(unittest.TestCase):
|
|||
folder = frappe.get_doc("File", "Home/Test Folder 1/Test Folder 3")
|
||||
self.assertRaises(frappe.ValidationError, folder.delete)
|
||||
|
||||
def test_same_file_url_update(self):
|
||||
attached_to_doctype1, attached_to_docname1 = make_test_doc()
|
||||
attached_to_doctype2, attached_to_docname2 = make_test_doc()
|
||||
|
||||
file1 = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": 'file1.txt',
|
||||
"attached_to_doctype": attached_to_doctype1,
|
||||
"attached_to_name": attached_to_docname1,
|
||||
"is_private": 1,
|
||||
"content": test_content1}).insert()
|
||||
|
||||
file2 = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": 'file2.txt',
|
||||
"attached_to_doctype": attached_to_doctype2,
|
||||
"attached_to_name": attached_to_docname2,
|
||||
"is_private": 1,
|
||||
"content": test_content1}).insert()
|
||||
|
||||
self.assertEqual(file1.is_private, file2.is_private, 1)
|
||||
self.assertEqual(file1.file_url, file2.file_url)
|
||||
self.assertTrue(os.path.exists(file1.get_full_path()))
|
||||
|
||||
file1.is_private = 0
|
||||
file1.save()
|
||||
|
||||
file2 = frappe.get_doc('File', file2.name)
|
||||
|
||||
self.assertEqual(file1.is_private, file2.is_private, 0)
|
||||
self.assertEqual(file1.file_url, file2.file_url)
|
||||
self.assertTrue(os.path.exists(file2.get_full_path()))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ class ModuleDef(Document):
|
|||
|
||||
def on_trash(self):
|
||||
"""Delete module name from modules.txt"""
|
||||
|
||||
if frappe.flags.in_uninstall:
|
||||
return
|
||||
|
||||
modules = None
|
||||
if frappe.local.module_app.get(frappe.scrub(self.name)):
|
||||
with open(frappe.get_app_path(self.app_name, "modules.txt"), "r") as f:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
frappe.ui.form.on('Report', {
|
||||
refresh: function(frm) {
|
||||
if(!frappe.boot.developer_mode && frappe.session.user !== 'Administrator') {
|
||||
if (frm.doc.is_standard && !frappe.boot.developer_mode) {
|
||||
// make the document read-only
|
||||
frm.set_read_only();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,16 +22,28 @@ class Role(Document):
|
|||
frappe.db.sql("delete from `tabHas Role` where role = %s", self.name)
|
||||
frappe.clear_cache()
|
||||
|
||||
def on_update(self):
|
||||
'''update system user desk access if this has changed in this update'''
|
||||
if frappe.flags.in_install: return
|
||||
if self.has_value_changed('desk_access'):
|
||||
for user_name in get_users(self.name):
|
||||
user = frappe.get_doc('User', user_name)
|
||||
user_type = user.user_type
|
||||
user.set_system_user()
|
||||
if user_type != user.user_type:
|
||||
user.save()
|
||||
|
||||
# Get email addresses of all users that have been assigned this role
|
||||
def get_emails_from_role(role):
|
||||
emails = []
|
||||
|
||||
users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"},
|
||||
fields=["parent"])
|
||||
|
||||
for user in users:
|
||||
user_email, enabled = frappe.db.get_value("User", user.parent, ["email", "enabled"])
|
||||
for user in get_users(role):
|
||||
user_email, enabled = frappe.db.get_value("User", user, ["email", "enabled"])
|
||||
if enabled and user_email not in ["admin@example.com", "guest@example.com"]:
|
||||
emails.append(user_email)
|
||||
|
||||
return emails
|
||||
return emails
|
||||
|
||||
def get_users(role):
|
||||
return [d.parent for d in frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"},
|
||||
fields=["parent"])]
|
||||
|
|
|
|||
|
|
@ -23,3 +23,28 @@ class TestUser(unittest.TestCase):
|
|||
|
||||
frappe.get_doc("User", "test@example.com").add_roles("_Test Role 3")
|
||||
self.assertTrue("_Test Role 3" in frappe.get_roles("test@example.com"))
|
||||
|
||||
def test_change_desk_access(self):
|
||||
'''if we change desk acecss from role, remove from user'''
|
||||
frappe.delete_doc_if_exists('User', 'test-user-for-desk-access@example.com')
|
||||
frappe.delete_doc_if_exists('Role', 'desk-access-test')
|
||||
user = frappe.get_doc(dict(
|
||||
doctype='User',
|
||||
email='test-user-for-desk-access@example.com',
|
||||
first_name='test')).insert()
|
||||
role = frappe.get_doc(dict(
|
||||
doctype = 'Role',
|
||||
role_name = 'desk-access-test',
|
||||
desk_access = 0
|
||||
)).insert()
|
||||
user.add_roles(role.name)
|
||||
user.save()
|
||||
self.assertTrue(user.user_type=='Website User')
|
||||
role.desk_access = 1
|
||||
role.save()
|
||||
user.reload()
|
||||
self.assertTrue(user.user_type=='System User')
|
||||
role.desk_access = 0
|
||||
role.save()
|
||||
user.reload()
|
||||
self.assertTrue(user.user_type=='Website User')
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
"column_break_18",
|
||||
"disable_standard_email_footer",
|
||||
"hide_footer_in_auto_email_reports",
|
||||
"attach_view_link",
|
||||
"chat",
|
||||
"enable_chat",
|
||||
"use_socketio_to_upload_file"
|
||||
|
|
@ -422,12 +423,18 @@
|
|||
"fieldname": "enable_onboarding",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Onboarding"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "attach_view_link",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send document Web View link in email"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-01 19:21:15.496065",
|
||||
"modified": "2020-07-02 16:13:00.166382",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import unicode_literals, print_function
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, flt, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
|
||||
from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
|
||||
from frappe import throw, msgprint, _
|
||||
from frappe.utils.password import update_password as _update_password
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
|
|
@ -770,7 +770,7 @@ def sign_up(email, full_name, redirect_to):
|
|||
user = frappe.get_doc({
|
||||
"doctype":"User",
|
||||
"email": email,
|
||||
"first_name": full_name,
|
||||
"first_name": escape_html(full_name),
|
||||
"enabled": 1,
|
||||
"new_password": random_string(10),
|
||||
"user_type": "Website User"
|
||||
|
|
@ -811,6 +811,7 @@ def reset_password(user):
|
|||
frappe.clear_messages()
|
||||
return 'not found'
|
||||
|
||||
@frappe.whitelist()
|
||||
def user_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
|
|
|
|||
|
|
@ -172,19 +172,26 @@ class Dashboard {
|
|||
set_dropdown() {
|
||||
this.page.clear_menu();
|
||||
|
||||
this.page.add_menu_item('Edit...', () => {
|
||||
this.page.add_menu_item(__('Edit'), () => {
|
||||
frappe.set_route('Form', 'Dashboard', frappe.dashboard.dashboard_name);
|
||||
}, 1);
|
||||
});
|
||||
|
||||
this.page.add_menu_item('New...', () => {
|
||||
this.page.add_menu_item(__('New'), () => {
|
||||
frappe.new_doc('Dashboard');
|
||||
}, 1);
|
||||
});
|
||||
|
||||
frappe.db.get_list("Dashboard").then(dashboards => {
|
||||
this.page.add_menu_item(__('Refresh All'), () => {
|
||||
this.chart_group &&
|
||||
this.chart_group.widgets_list.forEach(chart => chart.refresh());
|
||||
this.number_card_group &&
|
||||
this.number_card_group.widgets_list.forEach(card => card.render_card());
|
||||
});
|
||||
|
||||
frappe.db.get_list('Dashboard').then(dashboards => {
|
||||
dashboards.map(dashboard => {
|
||||
let name = dashboard.name;
|
||||
if(name != this.dashboard_name){
|
||||
this.page.add_menu_item(name, () => frappe.set_route("dashboard", name));
|
||||
this.page.add_menu_item(name, () => frappe.set_route("dashboard", name), 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,23 +5,23 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe import _, throw
|
||||
import frappe.utils.user
|
||||
from frappe.permissions import check_admin_or_system_manager
|
||||
from frappe.permissions import check_admin_or_system_manager, rights
|
||||
from frappe.model import data_fieldtypes
|
||||
|
||||
def execute(filters=None):
|
||||
user, doctype, show_permissions = filters.get("user"), filters.get("doctype"), filters.get("show_permissions")
|
||||
|
||||
if not validate(user, doctype): return [], []
|
||||
|
||||
columns, fields = get_columns_and_fields(doctype)
|
||||
data = frappe.get_list(doctype, fields=fields, as_list=True, user=user)
|
||||
|
||||
if show_permissions:
|
||||
columns = columns + ["Read", "Write", "Create", "Delete", "Submit", "Cancel", "Amend", "Print", "Email",
|
||||
"Report", "Import", "Export", "Share"]
|
||||
columns = columns + [frappe.unscrub(right) + ':Check:80' for right in rights]
|
||||
data = list(data)
|
||||
for i,item in enumerate(data):
|
||||
temp = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, item[0]), False,user)
|
||||
data[i] = item+(temp.get("read"),temp.get("write"),temp.get("create"),temp.get("delete"),temp.get("submit"),temp.get("cancel"),temp.get("amend"),temp.get("print"),temp.get("email"),temp.get("report"),temp.get("import"),temp.get("export"),temp.get("share"),)
|
||||
for i, doc in enumerate(data):
|
||||
permission = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, doc[0]), user)
|
||||
data[i] = doc + tuple(permission.get(right) for right in rights)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class DbManager:
|
|||
host = self.get_current_host()
|
||||
|
||||
if frappe.conf.get('rds_db', 0) == 1:
|
||||
self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO '%s'@'%s';" % (target, user, host))
|
||||
self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
|
||||
else:
|
||||
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
|
||||
|
||||
|
|
|
|||
|
|
@ -82,5 +82,7 @@ class MariaDBTable(DBTable):
|
|||
fieldname = str(e).split("'")[-2]
|
||||
frappe.throw(_("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format(
|
||||
fieldname, self.table_name))
|
||||
elif e.args[0]==1067:
|
||||
frappe.throw(str(e.args[1]))
|
||||
else:
|
||||
raise e
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import frappe, subprocess, os
|
||||
from six.moves import input
|
||||
|
||||
def setup_database(force, source_sql, verbose):
|
||||
def setup_database(force, source_sql=None, verbose=False):
|
||||
root_conn = get_root_connection()
|
||||
root_conn.commit()
|
||||
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name))
|
||||
|
|
@ -16,10 +16,12 @@ def setup_database(force, source_sql, verbose):
|
|||
subprocess_env = os.environ.copy()
|
||||
subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password)
|
||||
# bootstrap db
|
||||
if not source_sql:
|
||||
source_sql = os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
|
||||
|
||||
subprocess.check_output([
|
||||
'psql', frappe.conf.db_name, '-h', frappe.conf.db_host or 'localhost', '-U',
|
||||
frappe.conf.db_name, '-f',
|
||||
os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
|
||||
frappe.conf.db_name, '-f', source_sql
|
||||
], env=subprocess_env)
|
||||
|
||||
frappe.connect()
|
||||
|
|
|
|||
|
|
@ -29,31 +29,56 @@ def handle_not_exist(fn):
|
|||
|
||||
|
||||
class Workspace:
|
||||
def __init__(self, page_name):
|
||||
def __init__(self, page_name, minimal=False):
|
||||
self.page_name = page_name
|
||||
self.extended_cards = []
|
||||
self.extended_charts = []
|
||||
self.extended_shortcuts = []
|
||||
|
||||
self.user = frappe.get_user()
|
||||
self.allowed_modules = self.get_cached_value('user_allowed_modules', self.get_allowed_modules)
|
||||
self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules)
|
||||
|
||||
self.doc = self.get_page_for_user()
|
||||
|
||||
if self.doc.module not in self.allowed_modules:
|
||||
raise frappe.PermissionError
|
||||
|
||||
self.can_read = self.get_cached_value('user_perm_can_read', self.get_can_read_items)
|
||||
self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
|
||||
|
||||
self.allowed_pages = get_allowed_pages(cache=True)
|
||||
self.allowed_reports = get_allowed_reports(cache=True)
|
||||
self.onboarding_doc = self.get_onboarding_doc()
|
||||
self.onboarding = None
|
||||
|
||||
self.table_counts = get_table_with_counts()
|
||||
|
||||
if not minimal:
|
||||
self.onboarding_doc = self.get_onboarding_doc()
|
||||
self.onboarding = None
|
||||
|
||||
self.table_counts = get_table_with_counts()
|
||||
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
|
||||
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
|
||||
|
||||
def get_cached_value(self, cache_key, fallback_fn):
|
||||
def is_page_allowed(self):
|
||||
cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module) + self.extended_cards
|
||||
shortcuts = self.doc.shortcuts + self.extended_shortcuts
|
||||
|
||||
for section in cards:
|
||||
links = loads(section.links) if isinstance(section.links, string_types) else section.links
|
||||
for item in links:
|
||||
if self.is_item_allowed(item.get('name'), item.get('type')):
|
||||
return True
|
||||
|
||||
def _in_active_domains(item):
|
||||
if not item.restrict_to_domain:
|
||||
return True
|
||||
else:
|
||||
return item.restrict_to_domain in frappe.get_active_domains()
|
||||
|
||||
for item in shortcuts:
|
||||
if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_cached(self, cache_key, fallback_fn):
|
||||
_cache = frappe.cache()
|
||||
|
||||
value = _cache.get_value(cache_key, user=frappe.session.user)
|
||||
|
|
@ -83,12 +108,12 @@ class Workspace:
|
|||
'extends': self.page_name,
|
||||
'for_user': frappe.session.user
|
||||
}
|
||||
pages = frappe.get_list("Desk Page", filters=filters)
|
||||
pages = frappe.get_all("Desk Page", filters=filters, limit=1)
|
||||
if pages:
|
||||
return frappe.get_doc("Desk Page", pages[0])
|
||||
return frappe.get_cached_doc("Desk Page", pages[0])
|
||||
|
||||
self.get_pages_to_extend()
|
||||
return frappe.get_doc("Desk Page", self.page_name)
|
||||
return frappe.get_cached_doc("Desk Page", self.page_name)
|
||||
|
||||
def get_onboarding_doc(self):
|
||||
# Check if onboarding is enabled
|
||||
|
|
@ -123,7 +148,7 @@ class Workspace:
|
|||
'module': ['in', self.allowed_modules]
|
||||
})
|
||||
|
||||
pages = [frappe.get_doc("Desk Page", page['name']) for page in pages]
|
||||
pages = [frappe.get_cached_doc("Desk Page", page['name']) for page in pages]
|
||||
|
||||
for page in pages:
|
||||
self.extended_cards = self.extended_cards + page.cards
|
||||
|
|
@ -170,6 +195,7 @@ class Workspace:
|
|||
'docs_url': self.onboarding_doc.documentation_url,
|
||||
'items': self.get_onboarding_steps()
|
||||
}
|
||||
|
||||
@handle_not_exist
|
||||
def get_cards(self):
|
||||
cards = self.doc.cards
|
||||
|
|
@ -323,25 +349,44 @@ def get_desktop_page(page):
|
|||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_desk_sidebar_items(flatten=False):
|
||||
def get_desk_sidebar_items(flatten=False, cache=True):
|
||||
"""Get list of sidebar items for desk
|
||||
"""
|
||||
# don't get domain restricted pages
|
||||
blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
|
||||
pages = []
|
||||
_cache = frappe.cache()
|
||||
if cache:
|
||||
pages = _cache.get_value("desk_sidebar_items", user=frappe.session.user)
|
||||
|
||||
if not pages or not cache:
|
||||
# don't get domain restricted pages
|
||||
blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
|
||||
|
||||
filters = {
|
||||
'restrict_to_domain': ['in', frappe.get_active_domains()],
|
||||
'extends_another_page': 0,
|
||||
'for_user': '',
|
||||
'module': ['not in', blocked_modules]
|
||||
}
|
||||
filters = {
|
||||
'restrict_to_domain': ['in', frappe.get_active_domains()],
|
||||
'extends_another_page': 0,
|
||||
'for_user': '',
|
||||
'module': ['not in', blocked_modules]
|
||||
}
|
||||
|
||||
if not frappe.local.conf.developer_mode:
|
||||
filters['developer_mode_only'] = '0'
|
||||
if not frappe.local.conf.developer_mode:
|
||||
filters['developer_mode_only'] = '0'
|
||||
|
||||
# pages sorted based on pinned to top and then by name
|
||||
order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
|
||||
all_pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
|
||||
pages = []
|
||||
|
||||
# Filter Page based on Permission
|
||||
for page in all_pages:
|
||||
try:
|
||||
wspace = Workspace(page.get('name'), True)
|
||||
if wspace.is_page_allowed():
|
||||
pages.append(page)
|
||||
except frappe.PermissionError:
|
||||
pass
|
||||
|
||||
_cache.set_value("desk_sidebar_items", pages, frappe.session.user)
|
||||
|
||||
# pages sorted based on pinned to top and then by name
|
||||
order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
|
||||
pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
|
||||
if flatten:
|
||||
return pages
|
||||
|
||||
|
|
@ -375,7 +420,7 @@ def get_custom_reports_and_doctypes(module):
|
|||
]
|
||||
|
||||
def get_custom_doctype_list(module):
|
||||
doctypes = frappe.get_list("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name", ignore_permissions=True)
|
||||
doctypes = frappe.get_all("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name")
|
||||
|
||||
out = []
|
||||
for d in doctypes:
|
||||
|
|
@ -390,9 +435,9 @@ def get_custom_doctype_list(module):
|
|||
|
||||
def get_custom_report_list(module):
|
||||
"""Returns list on new style reports for modules."""
|
||||
reports = frappe.get_list("Report", fields=["name", "ref_doctype", "report_type"], filters=
|
||||
reports = frappe.get_all("Report", fields=["name", "ref_doctype", "report_type"], filters=
|
||||
{"is_standard": "No", "disabled": 0, "module": module},
|
||||
order_by="name", ignore_permissions=True)
|
||||
order_by="name")
|
||||
|
||||
out = []
|
||||
for r in reports:
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ frappe.ui.form.on('Dashboard', {
|
|||
refresh: function(frm) {
|
||||
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
|
||||
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.disable_form();
|
||||
}
|
||||
|
||||
frm.set_query("chart", "charts", function() {
|
||||
return {
|
||||
filters: {
|
||||
is_public: 1
|
||||
is_public: 1,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -16,7 +20,7 @@ frappe.ui.form.on('Dashboard', {
|
|||
frm.set_query("card", "cards", function() {
|
||||
return {
|
||||
filters: {
|
||||
is_public: 1
|
||||
is_public: 1,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:dashboard_name",
|
||||
"creation": "2019-01-10 12:54:40.938705",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -8,6 +9,8 @@
|
|||
"field_order": [
|
||||
"dashboard_name",
|
||||
"is_default",
|
||||
"is_standard",
|
||||
"module",
|
||||
"charts",
|
||||
"chart_options",
|
||||
"cards"
|
||||
|
|
@ -35,21 +38,35 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
|
||||
"fieldname": "chart_options",
|
||||
"fieldtype": "Code",
|
||||
"label": "Chart Options",
|
||||
"options": "JSON"
|
||||
"description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
|
||||
"fieldname": "chart_options",
|
||||
"fieldtype": "Code",
|
||||
"label": "Chart Options",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "cards",
|
||||
"fieldtype": "Table",
|
||||
"label": "Cards",
|
||||
"options": "Number Card Link"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.is_standard",
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
"mandatory_depends_on": "eval: doc.is_standard",
|
||||
"options": "Module Def"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-29 13:26:37.362482",
|
||||
"modified": "2020-07-10 17:48:19.468813",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
import frappe
|
||||
from frappe import _
|
||||
import json
|
||||
|
|
@ -15,7 +16,23 @@ class Dashboard(Document):
|
|||
frappe.db.sql('''update
|
||||
tabDashboard set is_default = 0 where name != %s''', self.name)
|
||||
|
||||
if frappe.conf.developer_mode and self.is_standard:
|
||||
export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module)
|
||||
|
||||
def validate(self):
|
||||
if not frappe.conf.developer_mode and self.is_standard:
|
||||
frappe.throw('Cannot edit Standard Dashboards')
|
||||
|
||||
if self.is_standard:
|
||||
non_standard_docs_map = {
|
||||
'Dashboard Chart': get_non_standard_charts_in_dashboard(self),
|
||||
'Number Card': get_non_standard_cards_in_dashboard(self)
|
||||
}
|
||||
|
||||
if non_standard_docs_map['Dashboard Chart'] or non_standard_docs_map['Number Card']:
|
||||
message = get_non_standard_warning_message(non_standard_docs_map)
|
||||
frappe.throw(message, title=_("Standard Not Set"), is_minimizable=True)
|
||||
|
||||
self.validate_custom_options()
|
||||
|
||||
def validate_custom_options(self):
|
||||
|
|
@ -48,3 +65,29 @@ def get_permitted_cards(dashboard_name):
|
|||
if frappe.has_permission('Number Card', doc=card.card):
|
||||
permitted_cards.append(card)
|
||||
return permitted_cards
|
||||
|
||||
def get_non_standard_charts_in_dashboard(dashboard):
|
||||
non_standard_charts = [doc.name for doc in frappe.get_list('Dashboard Chart', {'is_standard': 0})]
|
||||
return [chart_link.chart for chart_link in dashboard.charts if chart_link.chart in non_standard_charts]
|
||||
|
||||
def get_non_standard_cards_in_dashboard(dashboard):
|
||||
non_standard_cards = [doc.name for doc in frappe.get_list('Number Card', {'is_standard': 0})]
|
||||
return [card_link.card for card_link in dashboard.cards if card_link.card in non_standard_cards]
|
||||
|
||||
def get_non_standard_warning_message(non_standard_docs_map):
|
||||
message = _('''Please set the following documents in this Dashboard as standard first.''')
|
||||
|
||||
def get_html(docs, doctype):
|
||||
html = '<p>{}</p>'.format(frappe.bold(doctype))
|
||||
for doc in docs:
|
||||
html += '<div><a href="#Form/{doctype}/{doc}">{doc}</a></div>'.format(doctype=doctype, doc=doc)
|
||||
html += '<br>'
|
||||
return html
|
||||
|
||||
html = message + '<br>'
|
||||
|
||||
for doctype in non_standard_docs_map:
|
||||
if non_standard_docs_map[doctype]:
|
||||
html += get_html(non_standard_docs_map[doctype], doctype)
|
||||
|
||||
return html
|
||||
|
|
|
|||
|
|
@ -9,9 +9,24 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
frm.add_fetch('source', 'timeseries', 'timeseries');
|
||||
},
|
||||
|
||||
before_save: function(frm) {
|
||||
let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
|
||||
let static_filters = JSON.parse(frm.doc.filters_json || 'null');
|
||||
static_filters =
|
||||
frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
|
||||
|
||||
frm.set_value('filters_json', JSON.stringify(static_filters));
|
||||
frm.trigger('show_filters');
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.chart_filters = null;
|
||||
|
||||
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
|
||||
frm.set_df_property('chart_options_section', 'hidden', 1);
|
||||
frm.disable_form();
|
||||
}
|
||||
|
||||
frm.add_custom_button('Add Chart to Dashboard', () => {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Add to Dashboard'),
|
||||
|
|
@ -49,6 +64,8 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
});
|
||||
|
||||
frm.set_df_property("filters_section", "hidden", 1);
|
||||
frm.set_df_property("dynamic_filters_section", "hidden", 1);
|
||||
|
||||
frm.trigger('set_time_series');
|
||||
frm.set_query('document_type', function() {
|
||||
return {
|
||||
|
|
@ -66,6 +83,15 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
if (!frappe.boot.developer_mode) {
|
||||
frm.set_df_property("custom_options", "hidden", 1);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
is_standard: function(frm) {
|
||||
if (frappe.boot.developer_mode && frm.doc.is_standard) {
|
||||
frm.trigger('render_dynamic_filters_table');
|
||||
} else {
|
||||
frm.set_df_property("dynamic_filters_section", "hidden", 1);
|
||||
}
|
||||
},
|
||||
|
||||
source: function(frm) {
|
||||
|
|
@ -111,6 +137,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
frm.set_value('based_on', '');
|
||||
frm.set_value('value_based_on', '');
|
||||
frm.set_value('filters_json', '[]');
|
||||
frm.set_value('dynamic_filters_json', '[]');
|
||||
frm.trigger('update_options');
|
||||
},
|
||||
|
||||
|
|
@ -119,6 +146,8 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
frm.set_value('y_axis', []);
|
||||
frm.set_df_property('x_field', 'options', []);
|
||||
frm.set_value('filters_json', '{}');
|
||||
frm.set_value('dynamic_filters_json', '{}');
|
||||
frm.set_value('use_report_chart', 0);
|
||||
frm.trigger('set_chart_report_filters');
|
||||
},
|
||||
|
||||
|
|
@ -146,7 +175,10 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
},
|
||||
|
||||
set_chart_field_options: function(frm) {
|
||||
let filters = frm.doc.filters_json.length > 2? JSON.parse(frm.doc.filters_json): null;
|
||||
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
|
||||
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
|
||||
filters = frappe.dashboard_utils.get_all_filters(frm.doc);
|
||||
}
|
||||
frappe.xcall(
|
||||
'frappe.desk.query_report.run',
|
||||
{
|
||||
|
|
@ -156,16 +188,13 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
}
|
||||
).then(data => {
|
||||
frm.report_data = data;
|
||||
if (!data.chart) {
|
||||
frm.set_value('is_custom', 0);
|
||||
frm.set_df_property('is_custom', 'hidden', 1);
|
||||
} else {
|
||||
frm.set_df_property('is_custom', 'hidden', 0);
|
||||
}
|
||||
let report_has_chart = Boolean(data.chart);
|
||||
|
||||
if (!frm.doc.is_custom) {
|
||||
frm.set_df_property('use_report_chart', 'hidden', !report_has_chart);
|
||||
|
||||
if (!frm.doc.use_report_chart) {
|
||||
if (data.result.length) {
|
||||
frm.field_options = frappe.report_utils.get_possible_chart_options(data.columns, data);
|
||||
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
|
||||
frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields);
|
||||
if (!frm.field_options.numeric_fields.length) {
|
||||
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
|
||||
|
|
@ -240,11 +269,14 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
show_filters: function(frm) {
|
||||
frm.chart_filters = [];
|
||||
frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => {
|
||||
if (filters) {
|
||||
frm.chart_filters = filters;
|
||||
}
|
||||
if (filters) {
|
||||
frm.chart_filters = filters;
|
||||
}
|
||||
frm.trigger('render_filters_table');
|
||||
|
||||
frm.trigger('render_filters_table');
|
||||
if (frappe.boot.developer_mode && frm.doc.is_standard) {
|
||||
frm.trigger('render_dynamic_filters_table');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -257,8 +289,8 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">${__('Filter')}</th>
|
||||
<th style="width: 33%">${__('Condition')}</th>
|
||||
<th style="width: 20%">${__('Filter')}</th>
|
||||
<th style="width: 20%">${__('Condition')}</th>
|
||||
<th>${__('Value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -378,4 +410,102 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
});
|
||||
},
|
||||
|
||||
render_dynamic_filters_table(frm) {
|
||||
frm.set_df_property("dynamic_filters_section", "hidden", 0);
|
||||
|
||||
let is_document_type = frm.doc.chart_type !== 'Report'
|
||||
&& frm.doc.chart_type !== 'Custom';
|
||||
|
||||
let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
|
||||
|
||||
frm.dynamic_filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20%">${__('Filter')}</th>
|
||||
<th style="width: 20%">${__('Condition')}</th>
|
||||
<th>${__('Value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>`).appendTo(wrapper);
|
||||
|
||||
frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
|
||||
? JSON.parse(frm.doc.dynamic_filters_json)
|
||||
: null;
|
||||
|
||||
frm.trigger('set_dynamic_filters_in_table');
|
||||
|
||||
let filters = JSON.parse(frm.doc.filters_json || '[]');
|
||||
|
||||
let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
|
||||
is_document_type, filters, frm.dynamic_filters
|
||||
);
|
||||
|
||||
frm.dynamic_filter_table.on('click', () => {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Set Dynamic Filters'),
|
||||
fields: fields,
|
||||
primary_action: () => {
|
||||
let values = dialog.get_values();
|
||||
dialog.hide();
|
||||
let dynamic_filters = [];
|
||||
for (let key of Object.keys(values)) {
|
||||
if (is_document_type) {
|
||||
let [doctype, fieldname] = key.split(':');
|
||||
dynamic_filters.push([doctype, fieldname, '=', values[key]]);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_document_type) {
|
||||
frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
|
||||
} else {
|
||||
frm.set_value('dynamic_filters_json', JSON.stringify(values));
|
||||
}
|
||||
frm.trigger('set_dynamic_filters_in_table');
|
||||
},
|
||||
primary_action_label: "Set"
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
dialog.set_values(frm.dynamic_filters);
|
||||
});
|
||||
},
|
||||
|
||||
set_dynamic_filters_in_table: function(frm) {
|
||||
frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
|
||||
? JSON.parse(frm.doc.dynamic_filters_json)
|
||||
: null;
|
||||
|
||||
if (!frm.dynamic_filters) {
|
||||
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
|
||||
${__("Click to Set Dynamic Filters")}</td></tr>`);
|
||||
frm.dynamic_filter_table.find('tbody').html(filter_row);
|
||||
} else {
|
||||
let filter_rows = '';
|
||||
if ($.isArray(frm.dynamic_filters)) {
|
||||
frm.dynamic_filters.forEach(filter => {
|
||||
filter_rows +=
|
||||
`<tr>
|
||||
<td>${filter[1]}</td>
|
||||
<td>${filter[2] || ""}</td>
|
||||
<td>${filter[3]}</td>
|
||||
</tr>`;
|
||||
});
|
||||
} else {
|
||||
let condition = '=';
|
||||
for (let [key, val] of Object.entries(frm.dynamic_filters)) {
|
||||
filter_rows +=
|
||||
`<tr>
|
||||
<td>${key}</td>
|
||||
<td>${condition}</td>
|
||||
<td>${val || ""}</td>
|
||||
</tr>`
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
frm.dynamic_filter_table.find('tbody').html(filter_rows);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@
|
|||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"is_standard",
|
||||
"module",
|
||||
"chart_name",
|
||||
"chart_type",
|
||||
"report_name",
|
||||
"is_custom",
|
||||
"use_report_chart",
|
||||
"x_field",
|
||||
"y_axis",
|
||||
"source",
|
||||
|
|
@ -32,10 +34,12 @@
|
|||
"type",
|
||||
"filters_section",
|
||||
"filters_json",
|
||||
"dynamic_filters_section",
|
||||
"dynamic_filters_json",
|
||||
"chart_options_section",
|
||||
"color",
|
||||
"column_break_2",
|
||||
"custom_options",
|
||||
"column_break_2",
|
||||
"color",
|
||||
"section_break_10",
|
||||
"last_synced_on"
|
||||
],
|
||||
|
|
@ -67,7 +71,8 @@
|
|||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Document Type",
|
||||
"options": "DocType"
|
||||
"options": "DocType",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.timeseries && ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
|
||||
|
|
@ -189,32 +194,27 @@
|
|||
"label": "To Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom",
|
||||
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart",
|
||||
"fieldname": "x_field",
|
||||
"fieldtype": "Select",
|
||||
"label": "X Field",
|
||||
"mandatory_depends_on": "eval: doc.report_name && !doc.is_custom"
|
||||
"mandatory_depends_on": "eval: doc.report_name && !doc.use_report_chart"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.chart_type === 'Report'",
|
||||
"fieldname": "report_name",
|
||||
"fieldtype": "Link",
|
||||
"label": "Report Name",
|
||||
"options": "Report"
|
||||
"mandatory_depends_on": "eval:doc.chart_type === 'Report'",
|
||||
"options": "Report",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.report_name",
|
||||
"fieldname": "is_custom",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Custom"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom",
|
||||
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart",
|
||||
"fieldname": "y_axis",
|
||||
"fieldtype": "Table",
|
||||
"label": "Y Axis",
|
||||
"mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
|
||||
"mandatory_depends_on": "eval:doc.report_name && !doc.use_report_chart",
|
||||
"options": "Dashboard Chart Field"
|
||||
},
|
||||
{
|
||||
|
|
@ -235,10 +235,42 @@
|
|||
"fieldname": "heatmap_year",
|
||||
"fieldtype": "Select",
|
||||
"label": "Year"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.is_standard",
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
"mandatory_depends_on": "eval: doc.is_standard",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_filters_json",
|
||||
"fieldtype": "Code",
|
||||
"label": "Dynamic Filters JSON",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_filters_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dynamic Filters"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.report_name",
|
||||
"fieldname": "use_report_chart",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Report Chart"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-16 15:03:02.455395",
|
||||
"modified": "2020-07-21 16:37:07.763482",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate
|
|||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.boot import get_allowed_reports
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
|
|
@ -80,7 +81,9 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
|||
to_date = get_datetime(chart.to_date)
|
||||
|
||||
timegrain = time_interval or chart.time_interval
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
|
||||
if not filters:
|
||||
filters = []
|
||||
|
||||
# don't include cancelled documents
|
||||
filters.append([chart.document_type, 'docstatus', '<', 2, False])
|
||||
|
|
@ -259,7 +262,8 @@ def get_aggregate_function(chart_type):
|
|||
def get_result(data, timegrain, from_date, to_date):
|
||||
start_date = getdate(from_date)
|
||||
end_date = getdate(to_date)
|
||||
result = []
|
||||
|
||||
result = [[start_date, 0.0]]
|
||||
|
||||
while start_date < end_date:
|
||||
next_date = get_next_expected_date(start_date, timegrain)
|
||||
|
|
@ -277,11 +281,8 @@ def get_result(data, timegrain, from_date, to_date):
|
|||
|
||||
def get_next_expected_date(date, timegrain):
|
||||
next_date = None
|
||||
if timegrain=='Daily':
|
||||
next_date = add_to_date(date, days=1)
|
||||
else:
|
||||
# given date is always assumed to be the period ending date
|
||||
next_date = get_period_ending(add_to_date(date, days=1), timegrain)
|
||||
# given date is always assumed to be the period ending date
|
||||
next_date = get_period_ending(add_to_date(date, days=1), timegrain)
|
||||
return getdate(next_date)
|
||||
|
||||
def get_period_ending(date, timegrain):
|
||||
|
|
@ -349,8 +350,13 @@ class DashboardChart(Document):
|
|||
|
||||
def on_update(self):
|
||||
frappe.cache().delete_key('chart-data:{}'.format(self.name))
|
||||
if frappe.conf.developer_mode and self.is_standard:
|
||||
export_to_files(record_list=[['Dashboard Chart', self.name]], record_module=self.module)
|
||||
|
||||
|
||||
def validate(self):
|
||||
if not frappe.conf.developer_mode and self.is_standard:
|
||||
frappe.throw('Cannot edit Standard charts')
|
||||
if self.chart_type != 'Custom' and self.chart_type != 'Report':
|
||||
self.check_required_field()
|
||||
self.check_document_type()
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, frappe
|
||||
from frappe.utils import getdate, formatdate
|
||||
from frappe.utils import getdate, formatdate, get_last_day
|
||||
from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
|
||||
get_period_ending)
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import calendar
|
||||
|
||||
class TestDashboardChart(unittest.TestCase):
|
||||
def test_period_ending(self):
|
||||
|
|
@ -35,9 +34,6 @@ class TestDashboardChart(unittest.TestCase):
|
|||
self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'),
|
||||
getdate('2019-12-31'))
|
||||
|
||||
self.assertEqual(get_period_ending('2019-10-01', 'Yearly'),
|
||||
getdate('2019-12-31'))
|
||||
|
||||
def test_dashboard_chart(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart')
|
||||
|
|
@ -50,22 +46,24 @@ class TestDashboardChart(unittest.TestCase):
|
|||
based_on = 'creation',
|
||||
timespan = 'Last Year',
|
||||
time_interval = 'Monthly',
|
||||
filters_json = '[]',
|
||||
filters_json = '{}',
|
||||
timeseries = 1
|
||||
)).insert()
|
||||
|
||||
cur_date = datetime.now() - relativedelta(years=1)
|
||||
|
||||
result = get(chart_name ='Test Dashboard Chart', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
result = get(chart_name='Test Dashboard Chart', refresh=1)
|
||||
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
|
||||
|
||||
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
for idx in range(1, 13):
|
||||
month = get_last_day(cur_date)
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
# self.assertEqual(result.get('datasets')[0].get('values')[:-1],
|
||||
# [44, 28, 8, 11, 2, 6, 18, 6, 4, 5, 15, 13])
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_empty_dashboard_chart(self):
|
||||
|
|
@ -88,9 +86,14 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
cur_date = datetime.now() - relativedelta(years=1)
|
||||
|
||||
result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
result = get(chart_name ='Test Empty Dashboard Chart', refresh=1)
|
||||
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
|
||||
|
||||
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
for idx in range(1, 13):
|
||||
month = get_last_day(cur_date)
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
|
@ -121,8 +124,13 @@ class TestDashboardChart(unittest.TestCase):
|
|||
cur_date = datetime.now() - relativedelta(years=1)
|
||||
|
||||
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
|
||||
|
||||
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
for idx in range(1, 13):
|
||||
month = get_last_day(cur_date)
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
|
@ -132,6 +140,60 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_group_by_chart_type(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart')
|
||||
|
||||
frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert()
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Group By Dashboard Chart',
|
||||
chart_type = 'Group By',
|
||||
document_type = 'ToDo',
|
||||
group_by_based_on = 'status',
|
||||
filters_json = '[]',
|
||||
)).insert()
|
||||
|
||||
result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1)
|
||||
todo_status_count = frappe.db.count('ToDo', {'status': result.get('labels')[0]})
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values')[0], todo_status_count)
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_daily_dashboard_chart(self):
|
||||
insert_test_records()
|
||||
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Daily Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Daily Dashboard Chart')
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Daily Dashboard Chart',
|
||||
chart_type = 'Sum',
|
||||
document_type = 'Communication',
|
||||
based_on = 'communication_date',
|
||||
value_based_on = 'rating',
|
||||
timespan = 'Select Date Range',
|
||||
time_interval = 'Daily',
|
||||
from_date = datetime(2019, 1, 6),
|
||||
to_date = datetime(2019, 1, 11),
|
||||
filters_json = '[]',
|
||||
timeseries = 1
|
||||
)).insert()
|
||||
|
||||
result = get(chart_name ='Test Daily Dashboard Chart', refresh = 1)
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0])
|
||||
self.assertEqual(
|
||||
result.get('labels'),
|
||||
[formatdate('2019-01-06'), formatdate('2019-01-07'), formatdate('2019-01-08'),\
|
||||
formatdate('2019-01-09'), formatdate('2019-01-10'), formatdate('2019-01-11')]
|
||||
)
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_weekly_dashboard_chart(self):
|
||||
insert_test_records()
|
||||
|
||||
|
|
@ -155,37 +217,18 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0])
|
||||
self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_group_by_chart_type(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart')
|
||||
|
||||
frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert()
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Group By Dashboard Chart',
|
||||
chart_type = 'Group By',
|
||||
document_type = 'ToDo',
|
||||
group_by_based_on = 'status',
|
||||
filters_json = '[]',
|
||||
)).insert()
|
||||
|
||||
result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1)
|
||||
todo_status_count = frappe.db.count('ToDo', {'status': result.get('labels')[0]})
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values')[0], todo_status_count)
|
||||
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0])
|
||||
self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def insert_test_records():
|
||||
create_new_communication(datetime(2019, 1, 10), 100)
|
||||
create_new_communication(datetime(2018, 12, 30), 50)
|
||||
create_new_communication(datetime(2019, 1, 4), 100)
|
||||
create_new_communication(datetime(2019, 1, 6), 200)
|
||||
create_new_communication(datetime(2019, 1, 7), 400)
|
||||
create_new_communication(datetime(2019, 1, 8), 300)
|
||||
create_new_communication(datetime(2019, 1, 10), 100)
|
||||
|
||||
def create_new_communication(date, rating):
|
||||
communication = {
|
||||
|
|
|
|||
|
|
@ -1,162 +1,69 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"actions": [],
|
||||
"autoname": "field:source_name",
|
||||
"beta": 0,
|
||||
"creation": "2019-02-06 07:55:29.579840",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"source_name",
|
||||
"module",
|
||||
"timeseries"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "source_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": "Source 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,
|
||||
"translatable": 0,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "module",
|
||||
"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": "Module",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Module Def",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"default": "0",
|
||||
"fieldname": "timeseries",
|
||||
"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": "Timeseries",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Timeseries"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-04-09 14:20:51.548207",
|
||||
"links": [],
|
||||
"modified": "2020-06-26 18:00:37.421491",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart Source",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 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
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -18,10 +18,6 @@ def get_config(name):
|
|||
return f.read()
|
||||
|
||||
class DashboardChartSource(Document):
|
||||
def validate(self):
|
||||
if frappe.session.user != "Administrator":
|
||||
frappe.throw(_("Only Administrator is allowed to create Dashboard Chart Sources"))
|
||||
|
||||
def on_update(self):
|
||||
export_to_files(record_list=[[self.doctype, self.name]],
|
||||
record_module=self.module, create_init=True)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ def make_notification_logs(doc, users):
|
|||
_doc = frappe.new_doc('Notification Log')
|
||||
_doc.update(doc)
|
||||
_doc.for_user = user
|
||||
_doc.subject = _doc.subject.replace('<div>', '').replace('</div>', '')
|
||||
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point' or doc.type == 'Alert':
|
||||
_doc.insert(ignore_permissions=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,153 @@
|
|||
|
||||
frappe.ui.form.on('Number Card', {
|
||||
refresh: function(frm) {
|
||||
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
|
||||
frm.disable_form();
|
||||
}
|
||||
frm.set_df_property("filters_section", "hidden", 1);
|
||||
frm.set_df_property("dynamic_filters_section", "hidden", 1);
|
||||
frm.trigger('set_options');
|
||||
|
||||
if (!frm.doc.type) {
|
||||
frm.set_value('type', 'Document Type');
|
||||
}
|
||||
|
||||
if (frm.doc.type == 'Report' && frm.doc.report_name) {
|
||||
frm.trigger('set_report_filters');
|
||||
}
|
||||
|
||||
if (frm.doc.type == 'Custom') {
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.disable_form();
|
||||
}
|
||||
frm.filters = eval(frm.doc.filters_config);
|
||||
frm.trigger('set_filters_description');
|
||||
frm.trigger('set_method_description');
|
||||
frm.trigger('render_filters_table');
|
||||
}
|
||||
frm.trigger('create_add_to_dashboard_button');
|
||||
},
|
||||
|
||||
create_add_to_dashboard_button: function(frm) {
|
||||
frm.add_custom_button('Add Card to Dashboard', () => {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Add to Dashboard'),
|
||||
fields: [
|
||||
{
|
||||
label: __('Select Dashboard'),
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'dashboard',
|
||||
options: 'Dashboard',
|
||||
}
|
||||
],
|
||||
primary_action: (values) => {
|
||||
values.name = frm.doc.name;
|
||||
frappe.xcall(
|
||||
'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard',
|
||||
{
|
||||
args: values
|
||||
}
|
||||
).then(()=> {
|
||||
let dashboard_route_html =
|
||||
`<a href = "#dashboard/${values.dashboard}">${values.dashboard}</a>`;
|
||||
let message =
|
||||
__(`Number Card ${values.name} add to Dashboard ` + dashboard_route_html);
|
||||
|
||||
frappe.msgprint(message);
|
||||
});
|
||||
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
||||
if (!frm.doc.name) {
|
||||
frappe.msgprint(__('Please create Card first'));
|
||||
} else {
|
||||
d.show();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
before_save: function(frm) {
|
||||
let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
|
||||
let static_filters = JSON.parse(frm.doc.filters_json || 'null');
|
||||
static_filters =
|
||||
frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
|
||||
|
||||
frm.set_value('filters_json', JSON.stringify(static_filters));
|
||||
frm.trigger('render_filters_table');
|
||||
frm.trigger('render_dynamic_filters_table');
|
||||
},
|
||||
|
||||
is_standard: function(frm) {
|
||||
frm.trigger('render_dynamic_filters_table');
|
||||
frm.set_df_property("dynamic_filters_section", "hidden", 1);
|
||||
},
|
||||
|
||||
set_filters_description: function(frm) {
|
||||
if (frm.doc.type == 'Custom') {
|
||||
frm.fields_dict.filters_config.set_description(`
|
||||
Set the filters here. For example:
|
||||
<pre class="small text-muted">
|
||||
<code>
|
||||
[{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Account"),
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
reqd: 1
|
||||
}]
|
||||
</code></pre>`);
|
||||
}
|
||||
},
|
||||
|
||||
set_method_description: function(frm) {
|
||||
if (frm.doc.type == 'Custom') {
|
||||
frm.fields_dict.method.set_description(`
|
||||
Set the path to a whitelisted function that will return the number on the card in the format:
|
||||
<pre class="small text-muted">
|
||||
<code>
|
||||
{
|
||||
"value": value,
|
||||
"fieldtype": "Currency"
|
||||
}
|
||||
</code></pre>`);
|
||||
}
|
||||
},
|
||||
|
||||
type: function(frm) {
|
||||
frm.trigger('set_filters_description');
|
||||
if (frm.doc.type == 'Report') {
|
||||
frm.set_query('report_name', () => {
|
||||
return {
|
||||
filters: {
|
||||
'report_type': ['!=', 'Report Builder']
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
report_name: function(frm) {
|
||||
frm.set_value('filters_json', '{}');
|
||||
frm.set_value('dynamic_filters_json', '{}');
|
||||
frm.set_df_property('report_field', 'options', []);
|
||||
frm.trigger('set_report_filters');
|
||||
},
|
||||
|
||||
filters_config: function(frm) {
|
||||
frm.filters = eval(frm.doc.filters_config);
|
||||
const filter_values = frappe.report_utils.get_filter_values(frm.filters);
|
||||
frm.set_value('filters_json', JSON.stringify(filter_values));
|
||||
frm.trigger('render_filters_table');
|
||||
},
|
||||
|
||||
|
|
@ -17,11 +162,16 @@ frappe.ui.form.on('Number Card', {
|
|||
};
|
||||
});
|
||||
frm.set_value('filters_json', '[]');
|
||||
frm.set_value('dynamic_filters_json', '[]');
|
||||
frm.set_value('aggregate_function_based_on', '');
|
||||
frm.trigger('set_options');
|
||||
},
|
||||
|
||||
set_options: function(frm) {
|
||||
if (frm.doc.type !== 'Document Type') {
|
||||
return;
|
||||
}
|
||||
|
||||
let aggregate_based_on_fields = [];
|
||||
const doctype = frm.doc.document_type;
|
||||
|
||||
|
|
@ -40,80 +190,275 @@ frappe.ui.form.on('Number Card', {
|
|||
|
||||
frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields);
|
||||
});
|
||||
frm.trigger('render_filters_table');
|
||||
frm.trigger('render_dynamic_filters_table');
|
||||
}
|
||||
},
|
||||
|
||||
set_report_filters: function(frm) {
|
||||
const report_name = frm.doc.report_name;
|
||||
if (report_name) {
|
||||
frappe.report_utils.get_report_filters(report_name).then(filters => {
|
||||
if (filters) {
|
||||
frm.filters = filters;
|
||||
const filter_values = frappe.report_utils.get_filter_values(filters);
|
||||
if (frm.doc.filters_json.length <= 2) {
|
||||
frm.set_value('filters_json', JSON.stringify(filter_values));
|
||||
}
|
||||
}
|
||||
frm.trigger('render_filters_table');
|
||||
frm.trigger('set_report_field_options');
|
||||
frm.trigger('render_dynamic_filters_table');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
set_report_field_options: function(frm) {
|
||||
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
|
||||
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
|
||||
filters = frappe.dashboard_utils.get_all_filters(frm.doc);
|
||||
}
|
||||
frappe.xcall(
|
||||
'frappe.desk.query_report.run',
|
||||
{
|
||||
report_name: frm.doc.report_name,
|
||||
filters: filters,
|
||||
ignore_prepared_report: 1
|
||||
}
|
||||
).then(data => {
|
||||
if (data.result.length) {
|
||||
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
|
||||
frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields);
|
||||
if (!frm.field_options.numeric_fields.length) {
|
||||
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
|
||||
}
|
||||
} else {
|
||||
frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render_filters_table: function(frm) {
|
||||
frm.set_df_property("filters_section", "hidden", 0);
|
||||
let is_document_type = frm.doc.type == 'Document Type';
|
||||
let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default;
|
||||
|
||||
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
|
||||
frm.filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">${__('Filter')}</th>
|
||||
<th style="width: 33%">${__('Condition')}</th>
|
||||
<th style="width: 20%">${__('Filter')}</th>
|
||||
<th style="width: 20%">${__('Condition')}</th>
|
||||
<th>${__('Value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>`).appendTo(wrapper);
|
||||
$(`<p class="text-muted small">${__("Click table to edit")}</p>`).appendTo(wrapper);
|
||||
|
||||
let filters = JSON.parse(frm.doc.filters_json || '[]');
|
||||
let filters_set = false;
|
||||
|
||||
// Set dynamic filters for reports
|
||||
if (frm.doc.type == 'Report') {
|
||||
let set_filters = false;
|
||||
frm.filters.forEach(f => {
|
||||
if (is_dynamic_filter(f)) {
|
||||
filters[f.fieldname] = f.default;
|
||||
set_filters = true;
|
||||
}
|
||||
});
|
||||
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
|
||||
}
|
||||
|
||||
let fields;
|
||||
if (is_document_type) {
|
||||
fields = [
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'filter_area',
|
||||
}
|
||||
];
|
||||
|
||||
if (filters.length) {
|
||||
filters.forEach(filter => {
|
||||
const filter_row =
|
||||
$(`<tr>
|
||||
<td>${filter[1]}</td>
|
||||
<td>${filter[2] || ""}</td>
|
||||
<td>${filter[3]}</td>
|
||||
</tr>`);
|
||||
|
||||
table.find('tbody').append(filter_row);
|
||||
});
|
||||
filters_set = true;
|
||||
}
|
||||
} else if (frm.filters.length) {
|
||||
fields = frm.filters.filter(f => f.fieldname);
|
||||
fields.map(f => {
|
||||
if (filters[f.fieldname]) {
|
||||
let condition = '=';
|
||||
const filter_row =
|
||||
$(`<tr>
|
||||
<td>${f.label}</td>
|
||||
<td>${condition}</td>
|
||||
<td>${filters[f.fieldname] || ""}</td>
|
||||
</tr>`);
|
||||
table.find('tbody').append(filter_row);
|
||||
if (!filters_set) filters_set = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!filters_set) {
|
||||
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
|
||||
${__("Click to Set Filters")}</td></tr>`);
|
||||
table.find('tbody').append(filter_row);
|
||||
}
|
||||
|
||||
table.on('click', () => {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Set Filters'),
|
||||
fields: fields.filter(f => !is_dynamic_filter(f)),
|
||||
primary_action: function() {
|
||||
let values = this.get_values();
|
||||
if (values) {
|
||||
this.hide();
|
||||
if (is_document_type) {
|
||||
let filters = frm.filter_group.get_filters();
|
||||
frm.set_value('filters_json', JSON.stringify(filters));
|
||||
} else {
|
||||
frm.set_value('filters_json', JSON.stringify(values));
|
||||
}
|
||||
frm.trigger('render_filters_table');
|
||||
}
|
||||
},
|
||||
primary_action_label: "Set"
|
||||
});
|
||||
|
||||
if (is_document_type) {
|
||||
frm.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: dialog.get_field('filter_area').$wrapper,
|
||||
doctype: frm.doc.document_type,
|
||||
on_change: () => {},
|
||||
});
|
||||
filters && frm.filter_group.add_filters_to_filter_group(filters);
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
|
||||
if (frm.doc.type == 'Report') {
|
||||
//Set query report object so that it can be used while fetching filter values in the report
|
||||
frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
|
||||
frappe.query_reports[frm.doc.report_name]
|
||||
&& frappe.query_reports[frm.doc.report_name].onload
|
||||
&& frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
|
||||
}
|
||||
|
||||
dialog.set_values(filters);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
render_dynamic_filters_table(frm) {
|
||||
if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == 'Custom') {
|
||||
return;
|
||||
}
|
||||
|
||||
frm.set_df_property("dynamic_filters_section", "hidden", 0);
|
||||
|
||||
let is_document_type = frm.doc.type == 'Document Type';
|
||||
|
||||
let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
|
||||
|
||||
frm.dynamic_filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20%">${__('Filter')}</th>
|
||||
<th style="width: 20%">${__('Condition')}</th>
|
||||
<th>${__('Value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>`).appendTo(wrapper);
|
||||
|
||||
frm.filters = JSON.parse(frm.doc.filters_json || '[]');
|
||||
frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
|
||||
? JSON.parse(frm.doc.dynamic_filters_json)
|
||||
: null;
|
||||
|
||||
frm.trigger('set_filters_in_table');
|
||||
frm.trigger('set_dynamic_filters_in_table');
|
||||
|
||||
frm.filter_table.on('click', () => {
|
||||
let filters = JSON.parse(frm.doc.filters_json || '[]');
|
||||
|
||||
let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
|
||||
is_document_type, filters, frm.dynamic_filters
|
||||
);
|
||||
|
||||
frm.dynamic_filter_table.on('click', () => {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Set Filters'),
|
||||
fields: [{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'filter_area',
|
||||
}],
|
||||
primary_action: function() {
|
||||
let values = this.get_values();
|
||||
if (values) {
|
||||
this.hide();
|
||||
frm.filters = frm.filter_group.get_filters();
|
||||
frm.set_value('filters_json', JSON.stringify(frm.filters));
|
||||
frm.trigger('set_filters_in_table');
|
||||
title: __('Set Dynamic Filters'),
|
||||
fields: fields,
|
||||
primary_action: () => {
|
||||
let values = dialog.get_values();
|
||||
dialog.hide();
|
||||
let dynamic_filters = [];
|
||||
for (let key of Object.keys(values)) {
|
||||
if (is_document_type) {
|
||||
let [doctype, fieldname] = key.split(':');
|
||||
dynamic_filters.push([doctype, fieldname, '=', values[key]]);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_document_type) {
|
||||
frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
|
||||
} else {
|
||||
frm.set_value('dynamic_filters_json', JSON.stringify(values));
|
||||
}
|
||||
frm.trigger('set_dynamic_filters_in_table');
|
||||
},
|
||||
primary_action_label: "Set"
|
||||
});
|
||||
|
||||
frappe.dashboards.filters_dialog = dialog;
|
||||
|
||||
frm.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: dialog.get_field('filter_area').$wrapper,
|
||||
doctype: frm.doc.document_type,
|
||||
on_change: () => {},
|
||||
});
|
||||
|
||||
frm.filter_group.add_filters_to_filter_group(frm.filters);
|
||||
|
||||
dialog.show();
|
||||
dialog.set_values(frm.filters);
|
||||
dialog.set_values(frm.dynamic_filters);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
set_filters_in_table: function(frm) {
|
||||
if (!frm.filters.length) {
|
||||
set_dynamic_filters_in_table: function(frm) {
|
||||
frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
|
||||
? JSON.parse(frm.doc.dynamic_filters_json)
|
||||
: null;
|
||||
|
||||
if (!frm.dynamic_filters) {
|
||||
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
|
||||
${__("Click to Set Filters")}</td></tr>`);
|
||||
frm.filter_table.find('tbody').html(filter_row);
|
||||
${__("Click to Set Dynamic Filters")}</td></tr>`);
|
||||
frm.dynamic_filter_table.find('tbody').html(filter_row);
|
||||
} else {
|
||||
let filter_rows = '';
|
||||
frm.filters.forEach(filter => {
|
||||
filter_rows +=
|
||||
`<tr>
|
||||
<td>${filter[1]}</td>
|
||||
<td>${filter[2] || ""}</td>
|
||||
<td>${filter[3]}</td>
|
||||
</tr>`;
|
||||
if ($.isArray(frm.dynamic_filters)) {
|
||||
frm.dynamic_filters.forEach(filter => {
|
||||
filter_rows +=
|
||||
`<tr>
|
||||
<td>${filter[1]}</td>
|
||||
<td>${filter[2] || ""}</td>
|
||||
<td>${filter[3]}</td>
|
||||
</tr>`;
|
||||
});
|
||||
} else {
|
||||
let condition = '=';
|
||||
for (let [key, val] of Object.entries(frm.dynamic_filters)) {
|
||||
filter_rows +=
|
||||
`<tr>
|
||||
<td>${key}</td>
|
||||
<td>${condition}</td>
|
||||
<td>${val || ""}</td>
|
||||
</tr>`
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
frm.filter_table.find('tbody').html(filter_rows);
|
||||
frm.dynamic_filter_table.find('tbody').html(filter_rows);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,39 +1,53 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2020-04-15 18:06:39.444683",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"is_standard",
|
||||
"module",
|
||||
"label",
|
||||
"type",
|
||||
"report_name",
|
||||
"method",
|
||||
"function",
|
||||
"aggregate_function_based_on",
|
||||
"column_break_2",
|
||||
"document_type",
|
||||
"report_field",
|
||||
"report_function",
|
||||
"is_public",
|
||||
"custom_configuration_section",
|
||||
"filters_config",
|
||||
"stats_section",
|
||||
"show_percentage_stats",
|
||||
"stats_time_interval",
|
||||
"filters_section",
|
||||
"filters_json",
|
||||
"dynamic_filters_section",
|
||||
"dynamic_filters_json",
|
||||
"section_break_16",
|
||||
"color"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Document Type'",
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: doc.type == 'Document Type'",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.document_type",
|
||||
"depends_on": "eval: doc.type == 'Document Type'",
|
||||
"fieldname": "function",
|
||||
"fieldtype": "Select",
|
||||
"label": "Function",
|
||||
"options": "Count\nSum\nAverage\nMinimum\nMaximum",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: doc.type == 'Document Type'",
|
||||
"options": "Count\nSum\nAverage\nMinimum\nMaximum"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.function !== 'Count'",
|
||||
|
|
@ -92,13 +106,91 @@
|
|||
"options": "Daily\nWeekly\nMonthly\nYearly"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Document Type'",
|
||||
"fieldname": "stats_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Stats"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.is_standard",
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
"mandatory_depends_on": "eval: doc.is_standard",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_filters_json",
|
||||
"fieldtype": "Code",
|
||||
"label": "Dynamic Filters JSON",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_filters_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dynamic Filters Section"
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"options": "Document Type\nReport\nCustom"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Report'",
|
||||
"fieldname": "report_name",
|
||||
"fieldtype": "Link",
|
||||
"label": "Report Name",
|
||||
"mandatory_depends_on": "eval: doc.type == 'Report'",
|
||||
"options": "Report"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Report'",
|
||||
"fieldname": "report_field",
|
||||
"fieldtype": "Select",
|
||||
"label": "Field",
|
||||
"mandatory_depends_on": "eval: doc.type == 'Report'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Custom'",
|
||||
"fieldname": "method",
|
||||
"fieldtype": "Data",
|
||||
"label": "Method",
|
||||
"mandatory_depends_on": "eval: doc.type == 'Custom'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Custom'",
|
||||
"fieldname": "custom_configuration_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Custom Configuration"
|
||||
},
|
||||
{
|
||||
"fieldname": "filters_config",
|
||||
"fieldtype": "Code",
|
||||
"label": "Filters Configuration",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Report'",
|
||||
"fieldname": "report_function",
|
||||
"fieldtype": "Select",
|
||||
"label": "Function",
|
||||
"mandatory_depends_on": "eval: doc.type == 'Report'",
|
||||
"options": "Sum\nAverage\nMinimum\nMaximum"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-06 19:47:57.753574",
|
||||
"modified": "2020-07-18 17:08:22.882538",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Number Card",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import frappe
|
|||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.modules.export_file import export_to_files
|
||||
|
||||
class NumberCard(Document):
|
||||
def autoname(self):
|
||||
|
|
@ -16,6 +17,10 @@ class NumberCard(Document):
|
|||
if frappe.db.exists("Number Card", self.name):
|
||||
self.name = append_number_if_name_exists('Number Card', self.name)
|
||||
|
||||
def on_update(self):
|
||||
if frappe.conf.developer_mode and self.is_standard:
|
||||
export_to_files(record_list=[['Number Card', self.name]], record_module=self.module)
|
||||
|
||||
def get_permission_query_conditions(user=None):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
|
@ -47,7 +52,7 @@ def has_permission(doc, ptype, user):
|
|||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_result(doc, to_date=None):
|
||||
def get_result(doc, filters, to_date=None):
|
||||
doc = frappe.parse_json(doc)
|
||||
fields = []
|
||||
sql_function_map = {
|
||||
|
|
@ -65,10 +70,13 @@ def get_result(doc, to_date=None):
|
|||
else:
|
||||
fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)]
|
||||
|
||||
filters = frappe.parse_json(doc.filters_json)
|
||||
filters = frappe.parse_json(filters)
|
||||
|
||||
if not filters:
|
||||
filters = []
|
||||
|
||||
if to_date:
|
||||
filters.append([doc.document_type, 'creation', '<', to_date, False])
|
||||
filters.append([doc.document_type, 'creation', '<', to_date])
|
||||
|
||||
res = frappe.db.get_list(doc.document_type, fields=fields, filters=filters)
|
||||
number = res[0]['result'] if res else 0
|
||||
|
|
@ -76,7 +84,7 @@ def get_result(doc, to_date=None):
|
|||
return cint(number)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_percentage_difference(doc, result):
|
||||
def get_percentage_difference(doc, filters, result):
|
||||
doc = frappe.parse_json(doc)
|
||||
result = frappe.parse_json(result)
|
||||
|
||||
|
|
@ -85,13 +93,13 @@ def get_percentage_difference(doc, result):
|
|||
if not doc.get('show_percentage_stats'):
|
||||
return
|
||||
|
||||
previous_result = calculate_previous_result(doc)
|
||||
previous_result = calculate_previous_result(doc, filters)
|
||||
difference = (result - previous_result)/100.0
|
||||
|
||||
return difference
|
||||
|
||||
|
||||
def calculate_previous_result(doc):
|
||||
def calculate_previous_result(doc, filters):
|
||||
from frappe.utils import add_to_date
|
||||
|
||||
current_date = frappe.utils.now()
|
||||
|
|
@ -104,7 +112,7 @@ def calculate_previous_result(doc):
|
|||
else:
|
||||
previous_date = add_to_date(current_date, years=-1)
|
||||
|
||||
number = get_result(doc, previous_date)
|
||||
number = get_result(doc, filters, previous_date)
|
||||
return number
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -147,3 +155,22 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
|
|||
search_conditions=search_conditions,
|
||||
conditions=conditions
|
||||
), values)
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_number_card(args):
|
||||
card = create_number_card(args)
|
||||
args = frappe.parse_json(args)
|
||||
args.name = card.name
|
||||
if args.dashboard:
|
||||
add_card_to_dashboard(frappe.as_json(args))
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_card_to_dashboard(args):
|
||||
args = frappe.parse_json(args)
|
||||
|
||||
dashboard = frappe.get_doc('Dashboard', args.dashboard)
|
||||
dashboard_link = frappe.new_doc('Number Card Link')
|
||||
dashboard_link.card = args.name
|
||||
|
||||
dashboard.append('cards', dashboard_link)
|
||||
dashboard.save()
|
||||
|
|
@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
|
||||
def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
|
||||
"""
|
||||
Get all nested submitted linked doctype linkinfo
|
||||
|
||||
|
|
@ -31,26 +31,27 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
|
|||
if not docs:
|
||||
docs = []
|
||||
|
||||
if not linked:
|
||||
linked = {}
|
||||
if not visited:
|
||||
visited = {}
|
||||
|
||||
if doctype not in visited:
|
||||
visited[doctype] = []
|
||||
|
||||
if name in visited[doctype]:
|
||||
return
|
||||
|
||||
linkinfo = get_linked_doctypes(doctype)
|
||||
linked_docs = get_linked_docs(doctype, name, linkinfo)
|
||||
|
||||
link_count = 0
|
||||
visited[doctype].append(name)
|
||||
|
||||
for link_doctype, link_names in linked_docs.items():
|
||||
if link_doctype not in linked:
|
||||
linked[link_doctype] = []
|
||||
|
||||
for link in link_names:
|
||||
if link['name'] == name:
|
||||
continue
|
||||
|
||||
if linked and name in linked[link_doctype]:
|
||||
continue
|
||||
|
||||
linked[link_doctype].append(link['name'])
|
||||
|
||||
docinfo = link.update({"doctype": link_doctype})
|
||||
validated_doc = validate_linked_doc(docinfo)
|
||||
|
||||
|
|
@ -58,16 +59,15 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
|
|||
continue
|
||||
|
||||
link_count += 1
|
||||
if link.name in [doc.get("name") for doc in docs]:
|
||||
continue
|
||||
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs, linked)
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
"docstatus": link.docstatus,
|
||||
"link_count": links.get("count")
|
||||
})
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs, visited)
|
||||
if links:
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
"docstatus": link.docstatus,
|
||||
"link_count": links.get("count")
|
||||
})
|
||||
|
||||
# sort linked documents by ascending number of links
|
||||
docs.sort(key=lambda doc: doc.get("link_count"))
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
|||
"shared": frappe.share.get_users(doc.doctype, doc.name),
|
||||
"views": get_view_logs(doc.doctype, doc.name),
|
||||
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
|
||||
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
|
||||
"milestones": get_milestones(doc.doctype, doc.name),
|
||||
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
|
||||
"tags": get_tags(doc.doctype, doc.name),
|
||||
|
|
@ -277,3 +278,14 @@ def get_document_email(doctype, name):
|
|||
|
||||
def get_automatic_email_link():
|
||||
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
|
||||
|
||||
def get_additional_timeline_content(doctype, docname):
|
||||
contents = []
|
||||
hooks = frappe.get_hooks().get('additional_timeline_content', {})
|
||||
methods_for_all_doctype = hooks.get('*', [])
|
||||
methods_for_current_doctype = hooks.get(doctype, [])
|
||||
|
||||
for method in methods_for_all_doctype + methods_for_current_doctype:
|
||||
contents.extend(frappe.get_attr(method)(doctype, docname) or [])
|
||||
|
||||
return contents
|
||||
|
|
@ -18,12 +18,7 @@ def savedocs(doc, action):
|
|||
if doc.docstatus==1:
|
||||
doc.submit()
|
||||
else:
|
||||
try:
|
||||
doc.save()
|
||||
except frappe.NameError as e:
|
||||
doctype, name, original_exception = e if isinstance(e, tuple) else (doc.doctype or "", doc.name or "", None)
|
||||
frappe.msgprint(frappe._("{0} {1} already exists").format(doctype, name))
|
||||
raise
|
||||
doc.save()
|
||||
|
||||
# update recent documents
|
||||
run_onload(doc)
|
||||
|
|
|
|||
|
|
@ -14,13 +14,16 @@ def get_leaderboards():
|
|||
return leaderboards
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_energy_point_leaderboard(from_date, company = None, field = None, limit = None):
|
||||
def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None):
|
||||
filters = [
|
||||
['type', '!=', 'Review'],
|
||||
]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(['creation', 'between', [date_range[0], date_range[1]]])
|
||||
energy_point_users = frappe.db.get_all('Energy Point Log',
|
||||
fields = ['user as name', 'sum(points) as value'],
|
||||
filters = [
|
||||
['type', '!=', 'Review'],
|
||||
['creation', '>', from_date]
|
||||
],
|
||||
filters = filters,
|
||||
group_by = 'user',
|
||||
order_by = 'value desc'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class Leaderboard {
|
|||
this.timespans = [
|
||||
"This Week", "This Month", "This Quarter", "This Year",
|
||||
"Last Week", "Last Month", "Last Quarter", "Last Year",
|
||||
"All Time", "Select From Date"
|
||||
"All Time", "Select Date Range"
|
||||
];
|
||||
|
||||
// for saving current selected filters
|
||||
|
|
@ -113,7 +113,7 @@ class Leaderboard {
|
|||
return {"label": __(d), value: d };
|
||||
})
|
||||
);
|
||||
this.create_from_date_field();
|
||||
this.create_date_range_field();
|
||||
|
||||
this.type_select = this.page.add_select(__("Field"),
|
||||
this.options.selected_filter.map(d => {
|
||||
|
|
@ -123,12 +123,12 @@ class Leaderboard {
|
|||
|
||||
this.timespan_select.on("change", (e) => {
|
||||
this.options.selected_timespan = e.currentTarget.value;
|
||||
if (this.options.selected_timespan === 'Select From Date') {
|
||||
this.from_date_field.show();
|
||||
if (this.options.selected_timespan === 'Select Date Range') {
|
||||
this.date_range_field.show();
|
||||
} else {
|
||||
this.from_date_field.hide();
|
||||
this.make_request();
|
||||
this.date_range_field.hide();
|
||||
}
|
||||
this.make_request();
|
||||
});
|
||||
|
||||
this.type_select.on("change", (e) => {
|
||||
|
|
@ -137,21 +137,21 @@ class Leaderboard {
|
|||
});
|
||||
}
|
||||
|
||||
create_from_date_field() {
|
||||
create_date_range_field() {
|
||||
let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
|
||||
this.from_date_field = $(`<div class="from-date-field"></div>`).insertAfter(timespan_field).hide();
|
||||
this.date_range_field = $(`<div class="from-date-field"></div>`).insertAfter(timespan_field).hide();
|
||||
|
||||
let date_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'selected_from_date',
|
||||
placeholder: frappe.datetime.month_start(),
|
||||
default: frappe.datetime.month_start(),
|
||||
fieldtype: 'DateRange',
|
||||
fieldname: 'selected_date_range',
|
||||
placeholder: "Date Range",
|
||||
default: [frappe.datetime.month_start(), frappe.datetime.now_date()],
|
||||
input_class: 'input-sm',
|
||||
reqd: 1,
|
||||
change: () => {
|
||||
this.selected_from_date = date_field.get_value();
|
||||
if (this.selected_from_date) this.make_request();
|
||||
this.selected_date_range = date_field.get_value();
|
||||
if (this.selected_date_range) this.make_request();
|
||||
}
|
||||
},
|
||||
parent: $(this.parent).find('.from-date-field'),
|
||||
|
|
@ -225,7 +225,7 @@ class Leaderboard {
|
|||
frappe.call(
|
||||
this.leaderboard_config[this.options.selected_doctype].method,
|
||||
{
|
||||
'from_date': this.get_from_date(),
|
||||
'date_range': this.get_date_range(),
|
||||
'company': this.options.selected_company,
|
||||
'field': this.options.selected_filter_item,
|
||||
'limit': this.leaderboard_limit,
|
||||
|
|
@ -375,23 +375,22 @@ class Leaderboard {
|
|||
</li>`);
|
||||
}
|
||||
|
||||
get_from_date() {
|
||||
get_date_range() {
|
||||
let timespan = this.options.selected_timespan.toLowerCase();
|
||||
let current_date = frappe.datetime.now_date();
|
||||
let get_from_date = {
|
||||
"this week": frappe.datetime.week_start(),
|
||||
"this month": frappe.datetime.month_start(),
|
||||
"this quarter": frappe.datetime.quarter_start(),
|
||||
"this year": frappe.datetime.year_start(),
|
||||
"last week": frappe.datetime.add_days(current_date, -7),
|
||||
"last month": frappe.datetime.add_months(current_date, -1),
|
||||
"last quarter": frappe.datetime.add_months(current_date, -3),
|
||||
"last year": frappe.datetime.add_months(current_date, -12),
|
||||
"all time": "",
|
||||
"select from date": this.selected_from_date || frappe.datetime.month_start()
|
||||
let date_range_map = {
|
||||
"this week": [frappe.datetime.week_start(), current_date],
|
||||
"this month": [frappe.datetime.month_start(), current_date],
|
||||
"this quarter": [frappe.datetime.quarter_start(), current_date],
|
||||
"this year": [frappe.datetime.year_start(), current_date],
|
||||
"last week": [frappe.datetime.add_days(current_date, -7), current_date],
|
||||
"last month": [frappe.datetime.add_months(current_date, -1), current_date],
|
||||
"last quarter": [frappe.datetime.add_months(current_date, -3), current_date],
|
||||
"last year": [frappe.datetime.add_months(current_date, -12), current_date],
|
||||
"all time": null,
|
||||
"select date range": this.selected_date_range || [frappe.datetime.month_start(), current_date]
|
||||
}
|
||||
|
||||
return get_from_date[timespan];
|
||||
return date_range_map[timespan];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
|||
import frappe, json
|
||||
from frappe.utils import cstr, unique, cint
|
||||
from frappe.permissions import has_permission
|
||||
from frappe.handler import is_whitelisted
|
||||
from frappe import _
|
||||
from six import string_types
|
||||
import re
|
||||
|
|
@ -74,8 +75,17 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
|
|||
|
||||
if query and query.split()[0].lower()!="select":
|
||||
# by method
|
||||
frappe.response["values"] = frappe.call(query, doctype, txt,
|
||||
searchfield, start, page_length, filters, as_dict=as_dict)
|
||||
try:
|
||||
is_whitelisted(frappe.get_attr(query))
|
||||
frappe.response["values"] = frappe.call(query, doctype, txt,
|
||||
searchfield, start, page_length, filters, as_dict=as_dict)
|
||||
except Exception as e:
|
||||
if frappe.local.conf.developer_mode:
|
||||
raise e
|
||||
else:
|
||||
frappe.respond_as_web_page(title='Invalid Method', html='Method not found',
|
||||
indicator_color='red', http_status_code=404)
|
||||
return
|
||||
elif not query and doctype in standard_queries:
|
||||
# from standard queries
|
||||
search_widget(doctype, txt, standard_queries[doctype][0],
|
||||
|
|
|
|||
|
|
@ -95,6 +95,11 @@ frappe.ui.form.on("Email Account", {
|
|||
enable_incoming: function(frm) {
|
||||
frm.doc.no_remaining = null; //perform full sync
|
||||
//frm.set_df_property("append_to", "reqd", frm.doc.enable_incoming);
|
||||
frm.trigger("warn_autoreply_on_incoming");
|
||||
},
|
||||
|
||||
enable_auto_reply: function(frm) {
|
||||
frm.trigger("warn_autoreply_on_incoming");
|
||||
},
|
||||
|
||||
notify_if_unreplied: function(frm) {
|
||||
|
|
@ -184,7 +189,18 @@ frappe.ui.form.on("Email Account", {
|
|||
read as well as unread message from server. This may also cause the duplication\
|
||||
of Communication (emails).");
|
||||
frappe.confirm(msg, null, function() {
|
||||
frm.set_value("email_sync_option", "ALL");
|
||||
frm.set_value("email_sync_option", "UNSEEN");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
warn_autoreply_on_incoming: function(frm) {
|
||||
if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) {
|
||||
var msg = __("Enabling auto reply on an incoming email account will send automated replies \
|
||||
to all the synchronized emails. Do you wish to continue?");
|
||||
frappe.confirm(msg, null, function() {
|
||||
frm.set_value("enable_auto_reply", 0);
|
||||
frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,6 +274,8 @@ class EmailAccount(Document):
|
|||
|
||||
for idx, msg in enumerate(incoming_mails):
|
||||
uid = None if not uid_list else uid_list[idx]
|
||||
self.flags.notify = True
|
||||
|
||||
try:
|
||||
args = {
|
||||
"uid": uid,
|
||||
|
|
@ -294,7 +296,11 @@ class EmailAccount(Document):
|
|||
|
||||
else:
|
||||
frappe.db.commit()
|
||||
if communication:
|
||||
if communication and self.flags.notify:
|
||||
|
||||
# If email already exists in the system
|
||||
# then do not send notifications for the same email.
|
||||
|
||||
attachments = []
|
||||
|
||||
if hasattr(communication, '_attachments'):
|
||||
|
|
@ -363,6 +369,9 @@ class EmailAccount(Document):
|
|||
name = names[0].get("name")
|
||||
# email is already available update communication uid instead
|
||||
frappe.db.set_value("Communication", name, "uid", uid, update_modified=False)
|
||||
|
||||
self.flags.notify = False
|
||||
|
||||
return frappe.get_doc("Communication", name)
|
||||
|
||||
if email.content_type == 'text/html':
|
||||
|
|
@ -469,26 +478,38 @@ class EmailAccount(Document):
|
|||
|
||||
if self.append_to and self.sender_field:
|
||||
if self.subject_field:
|
||||
# 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(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
|
||||
"", email.subject, 0, flags=re.IGNORECASE)))
|
||||
if '#' in email.subject:
|
||||
# try and match if ID is found
|
||||
# document ID is appended to subject
|
||||
# example "Re: Your email (#OPP-2020-2334343)"
|
||||
parent_id = email.subject.rsplit('#', 1)[-1].strip(' ()')
|
||||
if parent_id:
|
||||
parent = frappe.db.get_all(self.append_to, filters = dict(name = parent_id),
|
||||
fields = 'name')
|
||||
|
||||
parent = frappe.db.get_all(self.append_to, filters={
|
||||
self.sender_field: email.from_email,
|
||||
self.subject_field: ("like", "%{0}%".format(subject)),
|
||||
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
|
||||
}, fields="name")
|
||||
if not parent:
|
||||
# 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(r"(^\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,
|
||||
self.subject_field: ("like", "%{0}%".format(subject)),
|
||||
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
|
||||
}, fields = "name", limit = 1)
|
||||
|
||||
# match only subject field
|
||||
# when the from_email is of a user in the system
|
||||
# and subject is atleast 10 chars long
|
||||
if not parent and len(subject) > 10 and is_system_user(email.from_email):
|
||||
# match only subject field
|
||||
# when the from_email is of a user in the system
|
||||
# and subject is atleast 10 chars long
|
||||
parent = frappe.db.get_all(self.append_to, filters={
|
||||
self.subject_field: ("like", "%{0}%".format(subject)),
|
||||
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
|
||||
}, fields="name")
|
||||
}, fields = "name", limit = 1)
|
||||
|
||||
|
||||
|
||||
if parent:
|
||||
parent = frappe._dict(doctype=self.append_to, name=parent[0].name)
|
||||
|
|
|
|||
|
|
@ -1,640 +1,166 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"beta": 0,
|
||||
"creation": "2012-08-02 15:17:28",
|
||||
"custom": 0,
|
||||
"description": "Email Queue records.",
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sender",
|
||||
"recipients",
|
||||
"show_as_cc",
|
||||
"message",
|
||||
"status",
|
||||
"error",
|
||||
"message_id",
|
||||
"reference_doctype",
|
||||
"reference_name",
|
||||
"communication",
|
||||
"send_after",
|
||||
"priority",
|
||||
"add_unsubscribe_link",
|
||||
"unsubscribe_param",
|
||||
"unsubscribe_method",
|
||||
"expose_recipients",
|
||||
"attachments",
|
||||
"retry"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "sender",
|
||||
"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,
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Sender",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Email",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "recipients",
|
||||
"fieldtype": "Table",
|
||||
"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": "Recipient",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Email Queue Recipient",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Email Queue Recipient"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "show_as_cc",
|
||||
"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": "Show as cc",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Show as cc"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "message",
|
||||
"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": "Message",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Message"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Not Sent",
|
||||
"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": 1,
|
||||
"label": "Status",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "\nNot Sent\nSending\nSent\nError\nExpired",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "\nNot Sent\nSending\nSent\nError\nExpired"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "error",
|
||||
"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": "Error",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Error"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "message_id",
|
||||
"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": "Message ID",
|
||||
"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": 1,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 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 Document Type",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_name",
|
||||
"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": "Reference DocName",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "communication",
|
||||
"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": "Communication",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Communication",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "send_after",
|
||||
"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": "Send After",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "priority",
|
||||
"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": "Priority",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "add_unsubscribe_link",
|
||||
"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": "Add Unsubscribe Link",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Add Unsubscribe Link"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "unsubscribe_param",
|
||||
"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": "Unsubscribe Param",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "unsubscribe_method",
|
||||
"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": "Unsubscribe Method",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Unsubscribe Method"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "expose_recipients",
|
||||
"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": "Expose Recipients",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Expose Recipients"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "attachments",
|
||||
"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": "Attachments",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "retry",
|
||||
"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": "Retry",
|
||||
"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
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-envelope",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 1,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-09-05 14:22:27.664645",
|
||||
"links": [],
|
||||
"modified": "2020-07-17 15:58:15.369419",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Queue",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"role": "System Manager"
|
||||
}
|
||||
],
|
||||
"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,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import email.utils
|
|||
from six import iteritems, text_type, string_types
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.header import Header
|
||||
from email import policy
|
||||
|
||||
|
||||
def get_email(recipients, sender='', msg='', subject='[No Subject]',
|
||||
|
|
@ -68,8 +69,8 @@ class EMail:
|
|||
self.subject = subject
|
||||
self.expose_recipients = expose_recipients
|
||||
|
||||
self.msg_root = MIMEMultipart('mixed')
|
||||
self.msg_alternative = MIMEMultipart('alternative')
|
||||
self.msg_root = MIMEMultipart('mixed', policy=policy.SMTPUTF8)
|
||||
self.msg_alternative = MIMEMultipart('alternative', policy=policy.SMTPUTF8)
|
||||
self.msg_root.attach(self.msg_alternative)
|
||||
self.cc = cc or []
|
||||
self.bcc = bcc or []
|
||||
|
|
@ -100,7 +101,7 @@ class EMail:
|
|||
Attach message in the text portion of multipart/alternative
|
||||
"""
|
||||
from email.mime.text import MIMEText
|
||||
part = MIMEText(message, 'plain', 'utf-8')
|
||||
part = MIMEText(message, 'plain', 'utf-8', policy=policy.SMTPUTF8)
|
||||
self.msg_alternative.attach(part)
|
||||
|
||||
def set_part_html(self, message, inline_images):
|
||||
|
|
@ -113,9 +114,9 @@ class EMail:
|
|||
message, _inline_images = replace_filename_with_cid(message)
|
||||
|
||||
# prepare parts
|
||||
msg_related = MIMEMultipart('related')
|
||||
msg_related = MIMEMultipart('related', policy=policy.SMTPUTF8)
|
||||
|
||||
html_part = MIMEText(message, 'html', 'utf-8')
|
||||
html_part = MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8)
|
||||
msg_related.attach(html_part)
|
||||
|
||||
for image in _inline_images:
|
||||
|
|
@ -124,7 +125,7 @@ class EMail:
|
|||
|
||||
self.msg_alternative.attach(msg_related)
|
||||
else:
|
||||
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8'))
|
||||
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8))
|
||||
|
||||
def set_html_as_text(self, html):
|
||||
"""Set plain text from HTML"""
|
||||
|
|
@ -135,7 +136,7 @@ class EMail:
|
|||
from email.mime.text import MIMEText
|
||||
|
||||
maintype, subtype = mime_type.split('/')
|
||||
part = MIMEText(message, _subtype = subtype)
|
||||
part = MIMEText(message, _subtype = subtype, policy=policy.SMTPUTF8)
|
||||
|
||||
if as_attachment:
|
||||
part.add_header('Content-Disposition', 'attachment', filename=filename)
|
||||
|
|
@ -222,7 +223,8 @@ class EMail:
|
|||
|
||||
# reset headers as values may be changed.
|
||||
for key, val in iteritems(headers):
|
||||
self.set_header(key, val)
|
||||
if val:
|
||||
self.set_header(key, val)
|
||||
|
||||
# call hook to enable apps to modify msg_root before sending
|
||||
for hook in frappe.get_hooks("make_email_body_message"):
|
||||
|
|
@ -238,7 +240,7 @@ class EMail:
|
|||
"""validate, build message and convert to string"""
|
||||
self.validate()
|
||||
self.make()
|
||||
return self.msg_root.as_string()
|
||||
return self.msg_root.as_string(policy=policy.SMTPUTF8)
|
||||
|
||||
def get_formatted_html(subject, message, footer=None, print_html=None,
|
||||
email_account=None, header=None, unsubscribe_link=None, sender=None):
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ def flush(from_test=False):
|
|||
if not smtpserver:
|
||||
smtpserver = SMTPServer()
|
||||
smtpserver_dict[email.sender] = smtpserver
|
||||
|
||||
|
||||
if from_test:
|
||||
send_one(email.name, smtpserver, auto_commit)
|
||||
else:
|
||||
|
|
@ -390,12 +390,12 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
|
|||
where
|
||||
name=%s
|
||||
for update''', email, as_dict=True)
|
||||
|
||||
|
||||
if len(email):
|
||||
email = email[0]
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
recipients_list = frappe.db.sql('''select name, recipient, status from
|
||||
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)
|
||||
|
||||
|
|
@ -417,6 +417,8 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
|
|||
if email.communication:
|
||||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
|
||||
|
||||
email_sent_to_any_recipient = None
|
||||
|
||||
try:
|
||||
message = None
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ This is the text version of this email
|
|||
subject='Test Subject',
|
||||
content=email_html,
|
||||
text_content=email_text
|
||||
).as_string()
|
||||
).as_string().replace("\r\n", "\n")
|
||||
|
||||
def test_prepare_message_returns_already_encoded_string(self):
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|||
subject='Test Subject',
|
||||
content=email_html,
|
||||
header=['Email Title', 'green']
|
||||
).as_string()
|
||||
).as_string().replace("\r\n", "\n")
|
||||
|
||||
self.assertTrue('''<span class=3D"indicator indicator-green" style=3D"background-color:#98=
|
||||
d85b; border-radius:8px; display:inline-block; height:8px; margin-right:5px=
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/frappe"
|
|||
app_license = "MIT"
|
||||
app_logo_url = '/assets/frappe/images/frappe-framework-logo.png'
|
||||
|
||||
develop_version = '12.x.x-develop'
|
||||
develop_version = '13.x.x-develop'
|
||||
|
||||
app_email = "info@frappe.io"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from __future__ import unicode_literals, print_function
|
|||
from six.moves import input
|
||||
|
||||
import os, json, subprocess, shutil
|
||||
import click
|
||||
import frappe
|
||||
import frappe.database
|
||||
import importlib
|
||||
|
|
@ -118,12 +119,20 @@ def remove_from_installed_apps(app_name):
|
|||
if frappe.flags.in_install:
|
||||
post_install()
|
||||
|
||||
def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
|
||||
def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
|
||||
"""Remove app and all linked to the app's module with the app from a site."""
|
||||
|
||||
# dont allow uninstall app if not installed unless forced
|
||||
if not force:
|
||||
if app_name not in frappe.get_installed_apps():
|
||||
click.secho("App {0} not installed on Site {1}".format(app_name, frappe.local.site), fg="yellow")
|
||||
return
|
||||
|
||||
print("Uninstalling App {0} from Site {1}...".format(app_name, frappe.local.site))
|
||||
|
||||
if not dry_run and not yes:
|
||||
confirm = input("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue (y/n) ? ")
|
||||
if confirm!="y":
|
||||
confirm = click.confirm("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue?")
|
||||
if not confirm:
|
||||
return
|
||||
|
||||
if not no_backup:
|
||||
|
|
@ -146,8 +155,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
|
|||
if not doctype.issingle:
|
||||
drop_doctypes.append(doctype.name)
|
||||
|
||||
# remove reports, pages and web forms
|
||||
for doctype in ("Report", "Page", "Web Form"):
|
||||
|
||||
linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent'])
|
||||
ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"]
|
||||
doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes]
|
||||
|
||||
for doctype in doctypes_with_linked_modules:
|
||||
for record in frappe.get_list(doctype, filters={"module": module_name}):
|
||||
print("removing {0} {1}...".format(doctype, record.name))
|
||||
if not dry_run:
|
||||
|
|
@ -166,6 +179,8 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
|
|||
for doctype in set(drop_doctypes):
|
||||
frappe.db.sql("drop table `tab{0}`".format(doctype))
|
||||
|
||||
click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green")
|
||||
|
||||
frappe.flags.in_uninstall = False
|
||||
|
||||
def post_install(rebuild_website=False):
|
||||
|
|
@ -333,3 +348,34 @@ def extract_tar_files(site_name, file_path, folder_name):
|
|||
frappe.destroy()
|
||||
|
||||
return tar_path
|
||||
|
||||
def is_downgrade(sql_file_path, verbose=False):
|
||||
"""checks if input db backup will get downgraded on current bench"""
|
||||
from semantic_version import Version
|
||||
head = "INSERT INTO `tabInstalled Application` VALUES"
|
||||
|
||||
with open(sql_file_path) as f:
|
||||
for line in f:
|
||||
if head in line:
|
||||
# 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master')
|
||||
line = line.strip().lstrip(head).rstrip(";").strip()
|
||||
# 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')]
|
||||
all_apps = [ x[-3:] for x in frappe.safe_eval(line) ]
|
||||
|
||||
for app in all_apps:
|
||||
app_name = app[0]
|
||||
app_version = app[1].split(" ")[0]
|
||||
|
||||
if app_name == "frappe":
|
||||
try:
|
||||
current_version = Version(frappe.__version__)
|
||||
backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
downgrade = backup_version > current_version
|
||||
|
||||
if verbose and downgrade:
|
||||
print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version))
|
||||
|
||||
return downgrade
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from frappe import _
|
|||
from frappe.model.document import Document
|
||||
from frappe.utils import get_request_site_address
|
||||
from googleapiclient.errors import HttpError
|
||||
from frappe.utils.password import set_encrypted_password
|
||||
from frappe.utils import add_days, get_datetime, get_weekdays, now_datetime, add_to_date, get_time_zone
|
||||
from dateutil import parser
|
||||
from datetime import datetime, timedelta
|
||||
|
|
@ -198,7 +199,7 @@ def check_google_calendar(account, google_calendar):
|
|||
except HttpError as err:
|
||||
frappe.throw(_("Google Calendar - Could not create Calendar for {0}, error code {1}.").format(account.name, err.resp.status))
|
||||
|
||||
def sync_events_from_google_calendar(g_calendar, method=None, page_length=10):
|
||||
def sync_events_from_google_calendar(g_calendar, method=None):
|
||||
"""
|
||||
Syncs Events from Google Calendar in Framework Calendar.
|
||||
Google Calendar returns nextSyncToken when all the events in Google Calendar are fetched.
|
||||
|
|
@ -210,23 +211,32 @@ def sync_events_from_google_calendar(g_calendar, method=None, page_length=10):
|
|||
if not account.pull_from_google_calendar:
|
||||
return
|
||||
|
||||
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
|
||||
events = frappe._dict()
|
||||
results = []
|
||||
while True:
|
||||
try:
|
||||
# API Response listed at EOF
|
||||
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
|
||||
events = google_calendar.events().list(calendarId=account.google_calendar_id, maxResults=page_length,
|
||||
singleEvents=False, showDeleted=True, syncToken=sync_token).execute()
|
||||
events = google_calendar.events().list(calendarId=account.google_calendar_id, maxResults=2000,
|
||||
pageToken=events.get("nextPageToken"), singleEvents=False, showDeleted=True, syncToken=sync_token).execute()
|
||||
except HttpError as err:
|
||||
frappe.throw(_("Google Calendar - Could not fetch event from Google Calendar, error code {0}.").format(err.resp.status))
|
||||
msg = _("Google Calendar - Could not fetch event from Google Calendar, error code {0}.").format(err.resp.status)
|
||||
|
||||
if err.resp.status == 410:
|
||||
set_encrypted_password("Google Calendar", account.name, "", "next_sync_token")
|
||||
frappe.db.commit()
|
||||
msg += ' ' + _('Sync token was invalid and has been resetted, Retry syncing.')
|
||||
frappe.msgprint(msg, title='Invalid Sync Token', indicator='blue')
|
||||
else:
|
||||
frappe.throw(msg)
|
||||
|
||||
for event in events.get("items", []):
|
||||
results.append(event)
|
||||
|
||||
if not events.get("nextPageToken"):
|
||||
if events.get("nextSyncToken"):
|
||||
frappe.db.set_value("Google Calendar", account.name, "next_sync_token", events.get("nextSyncToken"))
|
||||
frappe.db.commit()
|
||||
account.next_sync_token = events.get("nextSyncToken")
|
||||
account.save()
|
||||
break
|
||||
|
||||
for idx, event in enumerate(results):
|
||||
|
|
|
|||
|
|
@ -189,10 +189,10 @@ def upload_system_backup_to_google_drive():
|
|||
if frappe.flags.create_new_backup:
|
||||
set_progress(1, "Backing up Data.")
|
||||
backup = new_backup()
|
||||
fileurl_backup = os.path.basename(backup.backup_path_db)
|
||||
fileurl_site_config = os.path.basename(backup.site_config_backup_path)
|
||||
fileurl_public_files = os.path.basename(backup.backup_path_files)
|
||||
fileurl_private_files = os.path.basename(backup.backup_path_private_files)
|
||||
fileurl_backup = backup.backup_path_db
|
||||
fileurl_site_config = backup.site_config_backup_path
|
||||
fileurl_public_files = backup.backup_path_files
|
||||
fileurl_private_files = backup.backup_path_private_files
|
||||
else:
|
||||
fileurl_backup, fileurl_site_config, fileurl_public_files, fileurl_private_files = get_latest_backup_file(with_files=True)
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ def upload_system_backup_to_google_drive():
|
|||
try:
|
||||
media = MediaFileUpload(get_absolute_path(filename=fileurl), mimetype="application/gzip", resumable=True)
|
||||
except IOError as e:
|
||||
frappe.throw(_("Google Drive - Could not locate locate - {0}").format(e))
|
||||
frappe.throw(_("Google Drive - Could not locate - {0}").format(e))
|
||||
|
||||
try:
|
||||
set_progress(2, "Uploading backup to Google Drive.")
|
||||
|
|
@ -222,15 +222,17 @@ def upload_system_backup_to_google_drive():
|
|||
return _("Google Drive Backup Successful.")
|
||||
|
||||
def daily_backup():
|
||||
if frappe.db.get_single_value("Google Drive", "frequency") == "Daily":
|
||||
drive_settings = frappe.db.get_singles_dict('Google Drive')
|
||||
if drive_settings.enable and drive_settings.frequency == "Daily":
|
||||
upload_system_backup_to_google_drive()
|
||||
|
||||
def weekly_backup():
|
||||
if frappe.db.get_single_value("Google Drive", "frequency") == "Weekly":
|
||||
drive_settings = frappe.db.get_singles_dict('Google Drive')
|
||||
if drive_settings.enable and drive_settings.frequency == "Weekly":
|
||||
upload_system_backup_to_google_drive()
|
||||
|
||||
def get_absolute_path(filename):
|
||||
file_path = os.path.join(get_backups_path()[2:], filename)
|
||||
file_path = os.path.join(get_backups_path()[2:], os.path.basename(filename))
|
||||
return "{0}/sites/{1}".format(get_bench_path(), file_path)
|
||||
|
||||
def set_progress(progress, message):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ from botocore.exceptions import ClientError
|
|||
class S3BackupSettings(Document):
|
||||
|
||||
def validate(self):
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
if not self.endpoint_url:
|
||||
self.endpoint_url = 'https://s3.amazonaws.com'
|
||||
conn = boto3.client(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato
|
|||
|
||||
def migrate_to(local_site, frappe_provider):
|
||||
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
|
||||
return frappecloud_migrator(local_site, frappe_provider)
|
||||
return frappecloud_migrator(local_site)
|
||||
else:
|
||||
print("{} is not supported yet".format(frappe_provider))
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -1,412 +1,29 @@
|
|||
# imports - standard imports
|
||||
import getpass
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
from html2text import html2text
|
||||
import requests
|
||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||
from html2text import html2text
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
import frappe.utils.backups
|
||||
from frappe.utils import get_installed_apps_info
|
||||
from frappe.utils.commands import render_table, add_line_after, add_line_before
|
||||
|
||||
|
||||
# TODO: check upgrade compatibility
|
||||
|
||||
|
||||
def render_actions_table():
|
||||
actions_table = [["#", "Action"]]
|
||||
actions = []
|
||||
|
||||
for n, action in enumerate(migrator_actions):
|
||||
actions_table.append([n+1, action["title"]])
|
||||
actions.append(action["fn"])
|
||||
|
||||
render_table(actions_table)
|
||||
return actions
|
||||
|
||||
|
||||
def render_site_table(sites_info):
|
||||
sites_table = [["#", "Site Name", "Status"]]
|
||||
available_sites = []
|
||||
|
||||
for n, site_data in enumerate(sites_info):
|
||||
name, status = site_data["name"], site_data["status"]
|
||||
if status in ("Active", "Broken"):
|
||||
sites_table.append([n + 1, name, status])
|
||||
available_sites.append(name)
|
||||
|
||||
render_table(sites_table)
|
||||
return available_sites
|
||||
|
||||
|
||||
def render_teams_table(teams):
|
||||
teams_table = [["#", "Team"]]
|
||||
|
||||
for n, team in enumerate(teams):
|
||||
teams_table.append([n+1, team])
|
||||
|
||||
render_table(teams_table)
|
||||
|
||||
|
||||
def render_plan_table(plans_list):
|
||||
plans_table = [["Plan", "CPU Time"]]
|
||||
visible_headers = ["name", "cpu_time_per_day"]
|
||||
|
||||
for plan in plans_list:
|
||||
plan, cpu_time = [plan[header] for header in visible_headers]
|
||||
plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
|
||||
|
||||
render_table(plans_table)
|
||||
|
||||
|
||||
def render_group_table(app_groups):
|
||||
# title row
|
||||
app_groups_table = [["#", "App Group", "Apps"]]
|
||||
|
||||
# all rows
|
||||
for idx, app_group in enumerate(app_groups):
|
||||
apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
|
||||
row = [idx + 1, app_group["name"], apps_list]
|
||||
app_groups_table.append(row)
|
||||
|
||||
render_table(app_groups_table)
|
||||
|
||||
|
||||
def handle_request_failure(request=None, message=None, traceback=True, exit_code=1):
|
||||
message = message or "Request failed with error code {}".format(request.status_code)
|
||||
response = html2text(request.text) if traceback else ""
|
||||
|
||||
print("{0}{1}".format(message, "\n" + response))
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def select_primary_action():
|
||||
actions = render_actions_table()
|
||||
idx = click.prompt("What do you want to do?", type=click.IntRange(1, len(actions))) - 1
|
||||
|
||||
return actions[idx]
|
||||
|
||||
|
||||
@add_line_after
|
||||
def select_site():
|
||||
get_all_sites_request = session.post(all_site_url, headers={
|
||||
"accept": "application/json",
|
||||
"accept-encoding": "gzip, deflate, br",
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
})
|
||||
|
||||
if get_all_sites_request.ok:
|
||||
all_sites = get_all_sites_request.json()["message"]
|
||||
available_sites = render_site_table(all_sites)
|
||||
|
||||
while True:
|
||||
selected_site = click.prompt("Name of the site you want to restore to", type=str).strip()
|
||||
if selected_site in available_sites:
|
||||
return selected_site
|
||||
else:
|
||||
print("Site {} does not exist. Try again ❌".format(selected_site))
|
||||
else:
|
||||
print("Couldn't retrive sites list...Try again later")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@add_line_before
|
||||
def select_team(session):
|
||||
# get team options
|
||||
account_details_sc = session.post(account_details_url)
|
||||
if account_details_sc.ok:
|
||||
account_details = account_details_sc.json()["message"]
|
||||
available_teams = account_details["teams"]
|
||||
|
||||
# ask if they want to select, go ahead with if only one exists
|
||||
if len(available_teams) == 1:
|
||||
team = available_teams[0]
|
||||
else:
|
||||
render_teams_table(available_teams)
|
||||
idx = click.prompt("Select Team", type=click.IntRange(1, len(available_teams))) - 1
|
||||
team = available_teams[idx]
|
||||
|
||||
print("Team '{}' set for current session".format(team))
|
||||
|
||||
return team
|
||||
|
||||
|
||||
@retry(stop=stop_after_attempt(5))
|
||||
def get_new_site_options():
|
||||
site_options_sc = session.post(options_url)
|
||||
|
||||
if site_options_sc.ok:
|
||||
site_options = site_options_sc.json()["message"]
|
||||
return site_options
|
||||
else:
|
||||
print("Couldn't retrive New site information: {}".format(site_options_sc.status_code))
|
||||
|
||||
|
||||
def is_valid_subdomain(subdomain):
|
||||
if len(subdomain) < 5:
|
||||
print("Subdomain too short. Use 5 or more characters")
|
||||
return False
|
||||
matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain)
|
||||
if matched:
|
||||
return True
|
||||
print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens")
|
||||
|
||||
|
||||
@retry(stop=stop_after_attempt(5))
|
||||
def is_subdomain_available(subdomain):
|
||||
res = session.post(site_exists_url, {"subdomain": subdomain})
|
||||
if res.ok:
|
||||
available = not res.json()["message"]
|
||||
if not available:
|
||||
print("Subdomain already exists! Try another one")
|
||||
|
||||
return available
|
||||
|
||||
|
||||
@add_line_after
|
||||
def choose_plan(plans_list):
|
||||
print("{} plans available".format(len(plans_list)))
|
||||
available_plans = [plan["name"] for plan in plans_list]
|
||||
render_plan_table(plans_list)
|
||||
|
||||
while True:
|
||||
input_plan = click.prompt("Select Plan").strip()
|
||||
if input_plan in available_plans:
|
||||
print("{} Plan selected ✅".format(input_plan))
|
||||
return input_plan
|
||||
else:
|
||||
print("Invalid Selection ❌")
|
||||
|
||||
|
||||
@add_line_after
|
||||
def check_app_compat(available_group):
|
||||
is_compat = True
|
||||
incompatible_apps, filtered_apps, branch_msgs = [], [], []
|
||||
existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()]
|
||||
print("Checking availability of existing app group")
|
||||
|
||||
for (app, branch) in existing_group:
|
||||
info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ]
|
||||
if info:
|
||||
app_title, available_branch = info[0]
|
||||
|
||||
if branch != available_branch:
|
||||
print("⚠️ App {}:{} => {}".format(app, branch, available_branch))
|
||||
branch_msgs.append([app, branch, available_branch])
|
||||
filtered_apps.append(app_title)
|
||||
is_compat = False
|
||||
|
||||
else:
|
||||
print("✅ App {}:{}".format(app, branch))
|
||||
filtered_apps.append(app_title)
|
||||
|
||||
else:
|
||||
incompatible_apps.append(app)
|
||||
print("❌ App {}:{}".format(app, branch))
|
||||
is_compat = False
|
||||
|
||||
start_msg = "\nSelecting this group will "
|
||||
incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else ""
|
||||
branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else ""
|
||||
changes = (incompatible_apps + branch_change) or "be perfect for you :)"
|
||||
warning_message = start_msg + changes
|
||||
print(warning_message)
|
||||
|
||||
return is_compat, filtered_apps
|
||||
|
||||
|
||||
@add_line_after
|
||||
def filter_apps(app_groups):
|
||||
render_group_table(app_groups)
|
||||
|
||||
while True:
|
||||
app_group_index = click.prompt("Select App Group Number", type=int) - 1
|
||||
try:
|
||||
if app_group_index == -1:
|
||||
raise IndexError
|
||||
selected_group = app_groups[app_group_index]
|
||||
except IndexError:
|
||||
print("Invalid Selection ❌")
|
||||
continue
|
||||
|
||||
is_compat, filtered_apps = check_app_compat(selected_group)
|
||||
|
||||
if is_compat or click.confirm("Continue anyway?"):
|
||||
print("App Group {} selected! ✅".format(selected_group["name"]))
|
||||
break
|
||||
|
||||
return selected_group["name"], filtered_apps
|
||||
|
||||
|
||||
@add_line_after
|
||||
def get_subdomain(domain):
|
||||
while True:
|
||||
subdomain = click.prompt("Enter subdomain").strip()
|
||||
if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain):
|
||||
print("Site Domain: {}.{}".format(subdomain, domain))
|
||||
return subdomain
|
||||
|
||||
|
||||
@retry(stop=stop_after_attempt(2), wait=wait_fixed(5))
|
||||
def upload_backup_file(file_type, file_path):
|
||||
return session.post(files_url, data={}, files={
|
||||
"file": open(file_path, "rb"),
|
||||
"is_private": 1,
|
||||
"folder": "Home",
|
||||
"method": "press.api.site.upload_backup",
|
||||
"type": file_type
|
||||
})
|
||||
|
||||
|
||||
@add_line_after
|
||||
def upload_backup(local_site):
|
||||
# take backup
|
||||
files_session = {}
|
||||
print("Taking backup for site {}".format(local_site))
|
||||
odb = frappe.utils.backups.new_backup(ignore_files=False, force=True)
|
||||
|
||||
# upload files
|
||||
for x, (file_type, file_path) in enumerate([
|
||||
("database", odb.backup_path_db),
|
||||
("public", odb.backup_path_files),
|
||||
("private", odb.backup_path_private_files)
|
||||
]):
|
||||
file_name = file_path.split(os.sep)[-1]
|
||||
|
||||
print("Uploading {} file: {} ({}/3)".format(file_type, file_name, x+1))
|
||||
file_upload_response = upload_backup_file(file_type, file_path)
|
||||
|
||||
if file_upload_response.ok:
|
||||
files_session[file_type] = file_upload_response.json()["message"]
|
||||
else:
|
||||
print("Upload failed for: {}".format(file_path))
|
||||
|
||||
files_uploaded = { k: v["file_url"] for k, v in files_session.items() }
|
||||
print("Uploaded backup files! ✅")
|
||||
|
||||
return files_uploaded
|
||||
|
||||
|
||||
def new_site(local_site):
|
||||
# get new site options
|
||||
site_options = get_new_site_options()
|
||||
|
||||
# set preferences from site options
|
||||
subdomain = get_subdomain(site_options["domain"])
|
||||
plan = choose_plan(site_options["plans"])
|
||||
|
||||
app_groups = site_options["groups"]
|
||||
selected_group, filtered_apps = filter_apps(app_groups)
|
||||
files_uploaded = upload_backup(local_site)
|
||||
|
||||
# push to frappe_cloud
|
||||
payload = json.dumps({
|
||||
"site": {
|
||||
"apps": filtered_apps,
|
||||
"files": files_uploaded,
|
||||
"group": selected_group,
|
||||
"name": subdomain,
|
||||
"plan": plan
|
||||
}
|
||||
})
|
||||
|
||||
session.headers.update({"Content-Type": "application/json; charset=utf-8"})
|
||||
site_creation_request = session.post(upload_url, payload)
|
||||
|
||||
if site_creation_request.ok:
|
||||
site_url = site_creation_request.json()["message"]
|
||||
print("Your site {} is being migrated ✨".format(local_site))
|
||||
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
|
||||
print("Your site URL: {}".format(site_url))
|
||||
else:
|
||||
handle_request_failure(site_creation_request)
|
||||
|
||||
|
||||
def restore_site(local_site):
|
||||
# get list of existing sites they can restore
|
||||
selected_site = select_site()
|
||||
|
||||
# TODO: check if they can restore it
|
||||
|
||||
click.confirm("This is an irreversible action. Are you sure you want to continue?", abort=True)
|
||||
|
||||
# backup site
|
||||
files_uploaded = upload_backup(local_site)
|
||||
|
||||
# push to frappe_cloud
|
||||
payload = json.dumps({
|
||||
"name": selected_site,
|
||||
"files": files_uploaded
|
||||
})
|
||||
headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||
site_restore_request = session.post(restore_site_url, payload, headers=headers)
|
||||
|
||||
if site_restore_request.ok:
|
||||
print("Your site {0} is being restored on {1} ✨".format(local_site, selected_site))
|
||||
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, selected_site))
|
||||
print("Your site URL: {}".format(selected_site))
|
||||
else:
|
||||
handle_request_failure(site_restore_request)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def create_session():
|
||||
print("Frappe Cloud credentials @ {}".format(remote_site))
|
||||
|
||||
# take user input from STDIN
|
||||
username = click.prompt("Username").strip()
|
||||
password = getpass.unix_getpass()
|
||||
|
||||
auth_credentials = {"usr": username, "pwd": password}
|
||||
|
||||
session = requests.Session()
|
||||
login_sc = session.post(login_url, auth_credentials)
|
||||
|
||||
if login_sc.ok:
|
||||
print("Authorization Successful! ✅")
|
||||
team = select_team(session)
|
||||
session.headers.update({
|
||||
"X-Press-Team": team,
|
||||
"Connection": "keep-alive"
|
||||
})
|
||||
return session
|
||||
else:
|
||||
handle_request_failure(message="Authorization Failed with Error Code {}".format(login_sc.status_code), traceback=False)
|
||||
|
||||
|
||||
def frappecloud_migrator(local_site, frappecloud_site):
|
||||
global login_url, upload_url, files_url, options_url, site_exists_url, restore_site_url, account_details_url, all_site_url
|
||||
global session, migrator_actions, remote_site
|
||||
|
||||
def frappecloud_migrator(local_site):
|
||||
print("Retreiving Site Migrator...")
|
||||
remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
|
||||
request_url = "https://{}/api/method/press.api.script".format(remote_site)
|
||||
request = requests.get(request_url)
|
||||
|
||||
login_url = "https://{}/api/method/login".format(remote_site)
|
||||
upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
|
||||
files_url = "https://{}/api/method/upload_file".format(remote_site)
|
||||
options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
|
||||
site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
|
||||
account_details_url = "https://{}/api/method/press.api.account.get".format(remote_site)
|
||||
all_site_url = "https://{}/api/method/press.api.site.all".format(remote_site)
|
||||
restore_site_url = "https://{}/api/method/press.api.site.restore".format(remote_site)
|
||||
if request.status_code / 100 != 2:
|
||||
print("Request exitted with Status Code: {}\nPayload: {}".format(request.status_code, html2text(request.text)))
|
||||
click.secho("Some errors occurred while recovering the migration script. Please contact us @ Frappe Cloud if this issue persists", fg="yellow")
|
||||
return
|
||||
|
||||
migrator_actions = [
|
||||
{ "title": "Create a new site", "fn": new_site },
|
||||
{ "title": "Restore to an existing site", "fn": restore_site }
|
||||
]
|
||||
script_contents = request.json()["message"]
|
||||
|
||||
# get credentials + auth user + start session
|
||||
session = create_session()
|
||||
import tempfile
|
||||
import os
|
||||
import sys
|
||||
|
||||
# available actions defined in migrator_actions
|
||||
primary_action = select_primary_action()
|
||||
|
||||
primary_action(local_site)
|
||||
py = sys.executable
|
||||
script = tempfile.NamedTemporaryFile(mode="w")
|
||||
script.write(script_contents)
|
||||
print("Site Migrator stored at {}".format(script.name))
|
||||
os.execv(py, [py, script.name, local_site])
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ class BaseDocument(object):
|
|||
self.db_insert()
|
||||
return
|
||||
|
||||
frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name))
|
||||
frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
|
||||
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
|
||||
|
||||
elif frappe.db.is_unique_key_violation(e):
|
||||
|
|
@ -702,16 +702,13 @@ class BaseDocument(object):
|
|||
df = self.meta.get_field(fieldname)
|
||||
sanitized_value = value
|
||||
|
||||
if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email":
|
||||
sanitized_value = sanitize_email(value)
|
||||
if df and (df.get("ignore_xss_filter")
|
||||
or (df.get("fieldtype")=="Code" and df.get("options")!="Email")
|
||||
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode")
|
||||
|
||||
elif df and (df.get("ignore_xss_filter")
|
||||
or (df.get("fieldtype")=="Code" and df.get("options")!="Email")
|
||||
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode")
|
||||
|
||||
# cancelled and submit but not update after submit should be ignored
|
||||
or self.docstatus==2
|
||||
or (self.docstatus==1 and not df.get("allow_on_submit"))):
|
||||
# cancelled and submit but not update after submit should be ignored
|
||||
or self.docstatus==2
|
||||
or (self.docstatus==1 and not df.get("allow_on_submit"))):
|
||||
continue
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ def make_new_doc(doctype):
|
|||
doc = doc.get_valid_dict(sanitize=False)
|
||||
doc["doctype"] = doctype
|
||||
doc["__islocal"] = 1
|
||||
doc["__unsaved"] = 1
|
||||
|
||||
if not frappe.model.meta.is_single(doctype):
|
||||
doc["__unsaved"] = 1
|
||||
|
||||
return doc
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class DatabaseQuery(object):
|
|||
def sanitize_fields(self):
|
||||
'''
|
||||
regex : ^.*[,();].*
|
||||
purpose : The regex will look for malicious patterns like `,`, '(', ')', ';' in each
|
||||
purpose : The regex will look for malicious patterns like `,`, '(', ')', '@', ;' in each
|
||||
field which may leads to sql injection.
|
||||
example :
|
||||
field = "`DocType`.`issingle`, version()"
|
||||
|
|
@ -211,11 +211,11 @@ class DatabaseQuery(object):
|
|||
the system will filter out this field.
|
||||
'''
|
||||
|
||||
sub_query_regex = re.compile("^.*[,();].*")
|
||||
blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case']
|
||||
sub_query_regex = re.compile("^.*[,();@].*")
|
||||
blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case', 'show']
|
||||
blacklisted_functions = ['concat', 'concat_ws', 'if', 'ifnull', 'nullif', 'coalesce',
|
||||
'connection_id', 'current_user', 'database', 'last_insert_id', 'session_user',
|
||||
'system_user', 'user', 'version']
|
||||
'system_user', 'user', 'version', 'global']
|
||||
|
||||
def _raise_exception():
|
||||
frappe.throw(_('Use of sub-query or function is restricted'), frappe.DataError)
|
||||
|
|
@ -238,6 +238,10 @@ class DatabaseQuery(object):
|
|||
if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions):
|
||||
_raise_exception()
|
||||
|
||||
if '@' in field.lower():
|
||||
# prevent access to global variables
|
||||
_raise_exception()
|
||||
|
||||
if re.compile(r"[0-9a-zA-Z]+\s*'").match(field):
|
||||
_raise_exception()
|
||||
|
||||
|
|
@ -854,4 +858,4 @@ def get_date_range(operator, value):
|
|||
|
||||
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
|
||||
|
||||
return get_timespan_date_range(timespan)
|
||||
return get_timespan_date_range(timespan)
|
||||
|
|
|
|||
|
|
@ -396,11 +396,23 @@ class Document(BaseDocument):
|
|||
def get_doc_before_save(self):
|
||||
return getattr(self, '_doc_before_save', None)
|
||||
|
||||
def has_value_changed(self, fieldname):
|
||||
'''Returns true if value is changed before and after saving'''
|
||||
previous = self.get_doc_before_save()
|
||||
return previous.get(fieldname)!=self.get(fieldname) if previous else True
|
||||
|
||||
def set_new_name(self, force=False, set_name=None, set_child_names=True):
|
||||
"""Calls `frappe.naming.set_new_name` for parent and child docs."""
|
||||
|
||||
if self.flags.name_set and not force:
|
||||
return
|
||||
|
||||
# If autoname has set as Prompt (name)
|
||||
if self.get("__newname"):
|
||||
self.name = self.get("__newname")
|
||||
self.flags.name_set = True
|
||||
return
|
||||
|
||||
if set_name:
|
||||
self.name = set_name
|
||||
else:
|
||||
|
|
@ -825,7 +837,7 @@ class Document(BaseDocument):
|
|||
|
||||
def run_notifications(self, method):
|
||||
"""Run notifications for this method"""
|
||||
if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install:
|
||||
if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install:
|
||||
return
|
||||
|
||||
if self.flags.notifications_executed==None:
|
||||
|
|
|
|||
|
|
@ -12,16 +12,17 @@ def export_doc(doc):
|
|||
|
||||
def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None):
|
||||
"""
|
||||
Export record_list to files. record_list is a list of lists ([doctype],[docname] ) ,
|
||||
Export record_list to files. record_list is a list of lists ([doctype, docname, folder name],) ,
|
||||
"""
|
||||
if frappe.flags.in_import:
|
||||
return
|
||||
|
||||
if record_list:
|
||||
for record in record_list:
|
||||
write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init)
|
||||
folder_name = record[2] if len(record) == 3 else None
|
||||
write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init, folder_name=folder_name)
|
||||
|
||||
def write_document_file(doc, record_module=None, create_init=True):
|
||||
def write_document_file(doc, record_module=None, create_init=True, folder_name=None):
|
||||
newdoc = doc.as_dict(no_nulls=True)
|
||||
doc.run_method("before_export", newdoc)
|
||||
|
||||
|
|
@ -35,7 +36,10 @@ def write_document_file(doc, record_module=None, create_init=True):
|
|||
module = record_module or get_module_name(doc)
|
||||
|
||||
# create folder
|
||||
folder = create_folder(module, doc.doctype, doc.name, create_init)
|
||||
if folder_name:
|
||||
folder = create_folder(module, folder_name, doc.name, create_init)
|
||||
else:
|
||||
folder = create_folder(module, doc.doctype, doc.name, create_init)
|
||||
|
||||
# write the data file
|
||||
fname = scrub(doc.name)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ 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('email', 'doctype', 'document_follow')
|
||||
execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02
|
||||
execute:frappe.reload_doc('core', 'doctype', 'has_role')
|
||||
execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02
|
||||
frappe.patches.v11_0.replicate_old_user_permissions
|
||||
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
|
||||
|
|
@ -263,6 +264,7 @@ frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26
|
|||
frappe.patches.v12_0.setup_email_linking
|
||||
frappe.patches.v12_0.fix_home_settings_for_all_users
|
||||
frappe.patches.v12_0.change_existing_dashboard_chart_filters
|
||||
frappe.patches.v12_0.set_correct_assign_value_in_docs #2020-07-13
|
||||
execute:frappe.delete_doc("Test Runner")
|
||||
execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings')
|
||||
execute:frappe.db.set_default('desktop:home_page', 'workspace')
|
||||
|
|
@ -271,7 +273,9 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates')
|
|||
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
|
||||
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
|
||||
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
|
||||
frappe.patches.v12_0.remove_example_email_thread_notify
|
||||
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
|
||||
frappe.patches.v12_0.set_correct_url_in_files
|
||||
frappe.patches.v13_0.website_theme_custom_scss
|
||||
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
|
||||
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
|
||||
|
|
@ -289,4 +293,5 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Field")
|
|||
execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")
|
||||
frappe.patches.v13_0.update_date_filters_in_user_settings
|
||||
frappe.patches.v13_0.update_duration_options
|
||||
frappe.patches.v13_0.replace_old_data_import
|
||||
frappe.patches.v13_0.replace_old_data_import # 2020-06-24
|
||||
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
if frappe.db.exists('DocType', 'View log'):
|
||||
if frappe.db.table_exists('View log'):
|
||||
# for mac users direct renaming would not work since mysql for mac saves table name in lower case
|
||||
# so while renaming `tabView log` to `tabView Log` we get "Table 'tabView Log' already exists" error
|
||||
# more info https://stackoverflow.com/a/44753093/5955589 ,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
# remove all example.com email user accounts from notifications
|
||||
frappe.db.sql("""UPDATE `tabUser`
|
||||
SET thread_notify=0, send_me_a_copy=0
|
||||
WHERE email like '%@example.com'""")
|
||||
32
frappe/patches/v12_0/set_correct_assign_value_in_docs.py
Normal file
32
frappe/patches/v12_0/set_correct_assign_value_in_docs.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('desk', 'doctype', 'todo')
|
||||
|
||||
query = '''
|
||||
SELECT
|
||||
name, reference_type, reference_name, {} as assignees
|
||||
FROM
|
||||
`tabToDo`
|
||||
WHERE
|
||||
COALESCE(reference_type, '') != '' AND
|
||||
COALESCE(reference_name, '') != '' AND
|
||||
status != 'Cancelled'
|
||||
GROUP BY
|
||||
reference_type, reference_name
|
||||
'''
|
||||
|
||||
assignments = frappe.db.multisql({
|
||||
'mariadb': query.format('GROUP_CONCAT(DISTINCT `owner`)'),
|
||||
'postgres': query.format('STRING_AGG(DISTINCT "owner", ",")')
|
||||
}, as_dict=True)
|
||||
|
||||
for doc in assignments:
|
||||
assignments = doc.assignees.split(',')
|
||||
frappe.db.set_value(
|
||||
doc.reference_type,
|
||||
doc.reference_name,
|
||||
'_assign',
|
||||
frappe.as_json(assignments),
|
||||
update_modified=False
|
||||
)
|
||||
39
frappe/patches/v12_0/set_correct_url_in_files.py
Normal file
39
frappe/patches/v12_0/set_correct_url_in_files.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import frappe
|
||||
import os
|
||||
|
||||
def execute():
|
||||
files = frappe.get_all('File',
|
||||
fields = ['name', 'file_name', 'file_url'],
|
||||
filters = {
|
||||
'is_folder': 0,
|
||||
'file_url': ['!=', ''],
|
||||
})
|
||||
|
||||
private_file_path = frappe.get_site_path('private', 'files')
|
||||
public_file_path = frappe.get_site_path('public', 'files')
|
||||
|
||||
for file in files:
|
||||
file_path = file.file_url
|
||||
file_name = file_path.split('/')[-1]
|
||||
|
||||
if not file_path.startswith(('/private/', '/files/')):
|
||||
continue
|
||||
|
||||
file_is_private = file_path.startswith('/private/files/')
|
||||
full_path = frappe.utils.get_files_path(file_name, is_private=file_is_private)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
if file_is_private:
|
||||
public_file_url = os.path.join(public_file_path, file_name)
|
||||
if os.path.exists(public_file_url):
|
||||
frappe.db.set_value('File', file.name, {
|
||||
'file_url': '/files/{0}'.format(file_name),
|
||||
'is_private': 0
|
||||
})
|
||||
else:
|
||||
private_file_url = os.path.join(private_file_path, file_name)
|
||||
if os.path.exists(private_file_url):
|
||||
frappe.db.set_value('File', file.name, {
|
||||
'file_url': '/private/files/{0}'.format(file_name),
|
||||
'is_private': 1
|
||||
})
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import frappe
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.utils.dashboard import get_dashboards_with_link
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
def execute():
|
||||
if not frappe.db.table_exists('Dashboard Chart')\
|
||||
or not frappe.db.table_exists('Number Card')\
|
||||
or not frappe.db.table_exists('Dashboard'):
|
||||
return
|
||||
|
||||
|
||||
frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
|
||||
frappe.reload_doc('desk', 'doctype', 'number_card')
|
||||
frappe.reload_doc('desk', 'doctype', 'dashboard')
|
||||
|
||||
if frappe.db.has_column('Dashboard Chart', 'is_custom'):
|
||||
rename_field('Dashboard Chart', 'is_custom', 'use_report_chart')
|
||||
|
||||
modified_charts = get_modified_docs('Dashboard Chart')
|
||||
modified_cards = get_modified_docs('Number Card')
|
||||
modified_dashboards = [doc.name for doc in get_modified_docs('Dashboard')]
|
||||
|
||||
for chart in modified_charts:
|
||||
modified_dashboards += get_dashboards_with_link(chart.name, 'Dashboard Chart')
|
||||
rename_modified_doc(chart.name, 'Dashboard Chart')
|
||||
|
||||
for card in modified_cards:
|
||||
modified_dashboards += get_dashboards_with_link(card.name, 'Number Card')
|
||||
rename_modified_doc(card.name, 'Number Card')
|
||||
|
||||
modified_dashboards = list(set(modified_dashboards))
|
||||
|
||||
for dashboard in modified_dashboards:
|
||||
rename_modified_doc(dashboard, 'Dashboard')
|
||||
|
||||
def get_modified_docs(doctype):
|
||||
return frappe.get_all(doctype,
|
||||
filters = {
|
||||
'owner': 'Administrator',
|
||||
'modified_by': ['!=', 'Administrator']
|
||||
})
|
||||
|
||||
def rename_modified_doc(docname, doctype):
|
||||
new_name = docname + ' Custom'
|
||||
try:
|
||||
frappe.rename_doc(doctype, docname, new_name)
|
||||
except frappe.ValidationError:
|
||||
new_name = append_number_if_name_exists(doctype, new_name)
|
||||
frappe.rename_doc(doctype, docname, new_name)
|
||||
|
|
@ -6,9 +6,15 @@ import frappe
|
|||
|
||||
|
||||
def execute():
|
||||
frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy')
|
||||
if not frappe.db.table_exists("Data Import"): return
|
||||
|
||||
meta = frappe.get_meta("Data Import")
|
||||
# if Data Import is the new one, return early
|
||||
if meta.fields[1].fieldname == "import_type":
|
||||
return
|
||||
|
||||
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`")
|
||||
frappe.rename_doc("DocType", "Data Import", "Data Import Legacy")
|
||||
frappe.db.commit()
|
||||
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`")
|
||||
frappe.reload_doc("core", "doctype", "data_import")
|
||||
frappe.get_doc("DocType", "Data Import").on_update()
|
||||
frappe.delete_doc_if_exists("DocType", "Data Import Beta")
|
||||
frappe.rename_doc("DocType", "Data Import Beta", "Data Import")
|
||||
|
|
|
|||
|
|
@ -1,932 +1,203 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"actions": [],
|
||||
"creation": "2014-07-17 06:54:20.782907",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"pdf_settings",
|
||||
"send_print_as_pdf",
|
||||
"repeat_header_footer",
|
||||
"column_break_4",
|
||||
"pdf_page_size",
|
||||
"view_link_in_email",
|
||||
"with_letterhead",
|
||||
"allow_print_for_draft",
|
||||
"add_draft_heading",
|
||||
"column_break_10",
|
||||
"allow_page_break_inside_tables",
|
||||
"allow_print_for_cancelled",
|
||||
"server_printer",
|
||||
"enable_print_server",
|
||||
"server_ip",
|
||||
"printer_name",
|
||||
"port",
|
||||
"raw_printing_section",
|
||||
"enable_raw_printing",
|
||||
"print_style_section",
|
||||
"print_style",
|
||||
"print_style_preview",
|
||||
"section_break_8",
|
||||
"font",
|
||||
"font_size"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "pdf_settings",
|
||||
"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": "PDF Settings",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "PDF Settings"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "Send Email Print Attachments as PDF (Recommended)",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "send_print_as_pdf",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Send Print as PDF",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Send Print as PDF"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "repeat_header_footer",
|
||||
"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": "Repeat Header and Footer in PDF",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Repeat Header and Footer in PDF"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "A4",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "pdf_page_size",
|
||||
"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": "PDF Page Size",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "A4\nLetter",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "A4\nLetter"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "view_link_in_email",
|
||||
"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": "Page Settings",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Page Settings"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "with_letterhead",
|
||||
"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 with letterhead",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Print with letterhead"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "allow_print_for_draft",
|
||||
"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 Print for Draft",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Allow Print for Draft"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "attach_view_link",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Send document web view link in email",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "column_break_10",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "add_draft_heading",
|
||||
"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": "Always add \"Draft\" Heading for printing draft documents",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Always add \"Draft\" Heading for printing draft documents"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"default": "0",
|
||||
"fieldname": "allow_page_break_inside_tables",
|
||||
"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 page break inside tables",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Allow page break inside tables"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "",
|
||||
"fetch_if_empty": 0,
|
||||
"default": "0",
|
||||
"fieldname": "allow_print_for_cancelled",
|
||||
"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 Print for Cancelled",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Allow Print for Cancelled"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "server_printer",
|
||||
"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": "Print Server",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Print Server"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"default": "0",
|
||||
"fieldname": "enable_print_server",
|
||||
"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": "Enable Print Server",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Enable Print Server"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "localhost",
|
||||
"depends_on": "enable_print_server",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "server_ip",
|
||||
"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": "Server IP",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Server IP"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "enable_print_server",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "printer_name",
|
||||
"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": "Printer 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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Printer Name"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "631",
|
||||
"depends_on": "enable_print_server",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "port",
|
||||
"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": "Port",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Port"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "raw_printing_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": "Raw Printing",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Raw Printing"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"default": "0",
|
||||
"fieldname": "enable_raw_printing",
|
||||
"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": "Enable Raw Printing",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Enable Raw Printing"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "print_style_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": "Print Style",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Print Style"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Modern",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "print_style",
|
||||
"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": "Print Style",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Print Style",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Print Style"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "print_style_preview",
|
||||
"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": "Print Style Preview",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Print Style Preview"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 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,
|
||||
"label": "Fonts",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Fonts"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Default",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "font",
|
||||
"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": "Font",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Default\nArial\nHelvetica\nVerdana\nMonospace",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Default\nArial\nHelvetica\nVerdana\nMonospace"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "In points. Default is 9.",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "font_size",
|
||||
"fieldtype": "Float",
|
||||
"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": "Font Size",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Font Size"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": 0,
|
||||
"modified": "2019-04-10 14:12:31.081187",
|
||||
"links": [],
|
||||
"modified": "2020-07-02 16:14:47.470668",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Print Settings",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"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": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -7,6 +7,12 @@ body {
|
|||
p {
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
.ql-editor {
|
||||
white-space: normal;
|
||||
}
|
||||
.ql-editor p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
hr {
|
||||
border-top: 1px solid #d1d8dd;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,36 +13,6 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __('Export Data'),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
fieldname: 'exporting_for',
|
||||
label: __('Exporting For'),
|
||||
options: [
|
||||
{
|
||||
label: __('Insert New Records'),
|
||||
value: 'Insert New Records'
|
||||
},
|
||||
{
|
||||
label: __('Update Existing Records'),
|
||||
value: 'Update Existing Records'
|
||||
}
|
||||
],
|
||||
change: () => {
|
||||
let exporting_for = this.dialog.get_value('exporting_for');
|
||||
this.dialog.set_value(
|
||||
'export_records',
|
||||
exporting_for === 'Insert New Records' ? 'blank_template' : 'all'
|
||||
);
|
||||
|
||||
// Force ID field to be exported when updating existing records
|
||||
let id_field = this.dialog.get_field(this.doctype).options[0];
|
||||
if (id_field.value === 'name' && id_field.$checkbox) {
|
||||
id_field.$checkbox
|
||||
.find('input')
|
||||
.prop('disabled', exporting_for === 'Update Existing Records');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
fieldname: 'export_records',
|
||||
|
|
@ -65,7 +35,7 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
value: 'blank_template'
|
||||
}
|
||||
],
|
||||
default: 'blank_template',
|
||||
default: this.exporting_for === 'Insert New Records' ? 'blank_template' : 'all',
|
||||
change: () => {
|
||||
this.update_record_count_message();
|
||||
}
|
||||
|
|
@ -119,10 +89,6 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
on_page_show: () => this.select_mandatory()
|
||||
});
|
||||
|
||||
if (this.exporting_for) {
|
||||
this.dialog.set_value('exporting_for', this.exporting_for);
|
||||
}
|
||||
|
||||
this.make_filter_area();
|
||||
this.make_select_all_buttons();
|
||||
this.update_record_count_message();
|
||||
|
|
@ -172,15 +138,17 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
}
|
||||
|
||||
make_select_all_buttons() {
|
||||
let for_insert = this.exporting_for === 'Insert New Records';
|
||||
let section_title = for_insert ? __('Select Fields To Insert') : __('Select Fields To Update');
|
||||
let $select_all_buttons = $(`
|
||||
<div>
|
||||
<h6 class="form-section-heading uppercase">${__('Select fields to export')}</h6>
|
||||
<h6 class="form-section-heading uppercase">${section_title}</h6>
|
||||
<button class="btn btn-default btn-xs" data-action="select_all">
|
||||
${__('Select All')}
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs" data-action="select_mandatory">
|
||||
${for_insert ? `<button class="btn btn-default btn-xs" data-action="select_mandatory">
|
||||
${__('Select Mandatory')}
|
||||
</button>
|
||||
</button>`: ''}
|
||||
<button class="btn btn-default btn-xs" data-action="unselect_all">
|
||||
${__('Unselect All')}
|
||||
</button>
|
||||
|
|
@ -285,11 +253,9 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
}
|
||||
|
||||
get_filters() {
|
||||
return this.filter_group.get_filters().reduce((acc, filter) => {
|
||||
return Object.assign(acc, {
|
||||
[filter[1]]: [filter[2], filter[3]]
|
||||
});
|
||||
}, {});
|
||||
return this.filter_group.get_filters().map(filter => {
|
||||
return filter.slice(0, 4);
|
||||
});
|
||||
}
|
||||
|
||||
get_multicheck_options(doctype, child_fieldname = null) {
|
||||
|
|
@ -308,6 +274,9 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
? this.column_map[child_fieldname]
|
||||
: this.column_map[doctype];
|
||||
|
||||
let is_field_mandatory = df => (df.fieldname === 'name' && !child_fieldname)
|
||||
|| (df.reqd && this.exporting_for == 'Insert New Records');
|
||||
|
||||
return fields
|
||||
.filter(df => {
|
||||
if (autoname_field && df.fieldname === autoname_field.fieldname) {
|
||||
|
|
@ -323,7 +292,7 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
return {
|
||||
label,
|
||||
value: df.fieldname,
|
||||
danger: df.reqd,
|
||||
danger: is_field_mandatory(df),
|
||||
checked: false,
|
||||
description: `${df.fieldname} ${df.reqd ? __('(Mandatory)') : ''}`
|
||||
};
|
||||
|
|
|
|||
|
|
@ -245,11 +245,12 @@ frappe.data_import.ImportPreview = class ImportPreview {
|
|||
let fieldname;
|
||||
if (!df) {
|
||||
fieldname = null;
|
||||
} else if (col.map_to_field) {
|
||||
fieldname = col.map_to_field;
|
||||
} else if (col.is_child_table_field) {
|
||||
fieldname = `${col.child_table_df.fieldname}.${df.fieldname}`;
|
||||
} else {
|
||||
fieldname =
|
||||
df.parent === this.doctype
|
||||
? df.fieldname
|
||||
: `${df.parent}:${df.fieldname}`;
|
||||
fieldname = df.fieldname;
|
||||
}
|
||||
return [
|
||||
{
|
||||
|
|
@ -272,7 +273,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
|
|||
label: __("Don't Import"),
|
||||
value: "Don't Import"
|
||||
}
|
||||
].concat(column_picker_fields.get_fields_as_options()),
|
||||
].concat(get_fields_as_options(this.doctype, column_picker_fields)),
|
||||
default: fieldname || "Don't Import",
|
||||
change() {
|
||||
changed.push(i);
|
||||
|
|
@ -328,3 +329,29 @@ frappe.data_import.ImportPreview = class ImportPreview {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
function get_fields_as_options(doctype, column_map) {
|
||||
let keys = [doctype];
|
||||
frappe.meta.get_table_fields(doctype).forEach(df => {
|
||||
keys.push(df.fieldname);
|
||||
});
|
||||
// flatten array
|
||||
return [].concat(
|
||||
...keys.map(key => {
|
||||
return column_map[key].map(df => {
|
||||
let label = df.label;
|
||||
let value = df.fieldname;
|
||||
if (doctype !== key) {
|
||||
let table_field = frappe.meta.get_docfield(doctype, key);
|
||||
label = `${df.label} (${table_field.label})`;
|
||||
value = `${table_field.fieldname}.${df.fieldname}`;
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value,
|
||||
description: value
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -91,12 +91,26 @@ frappe.db = {
|
|||
});
|
||||
},
|
||||
count: function(doctype, args={}) {
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: 'frappe.client.get_count',
|
||||
type: 'GET',
|
||||
args: Object.assign(args, { doctype })
|
||||
}).then(r => resolve(r.message));
|
||||
let filters = args.filters || {};
|
||||
const with_child_table_filter = Array.isArray(filters) && filters.some(filter => {
|
||||
return filter[0] !== doctype;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
// cannot break this line as it adds extra \n's and \t's which breaks the query
|
||||
`count(${with_child_table_filter ? 'distinct': ''} ${frappe.model.get_full_column_name('name', doctype)}) AS total_count`
|
||||
];
|
||||
|
||||
return frappe.call({
|
||||
type: 'GET',
|
||||
method: 'frappe.desk.reportview.get',
|
||||
args: {
|
||||
doctype,
|
||||
filters,
|
||||
fields,
|
||||
}
|
||||
}).then(r => {
|
||||
return r.message.values[0][0];
|
||||
});
|
||||
},
|
||||
get_link_options(doctype, txt = '', filters={}) {
|
||||
|
|
|
|||
|
|
@ -101,15 +101,6 @@ frappe.Application = Class.extend({
|
|||
frappe.ui.startup_setup_dialog.show();
|
||||
}
|
||||
|
||||
// listen to csrf_update
|
||||
frappe.realtime.on("csrf_generated", function(data) {
|
||||
// handles the case when a user logs in again from another tab
|
||||
// and it leads to invalid request in the current tab
|
||||
if (data.csrf_token && data.sid===frappe.get_cookie("sid")) {
|
||||
frappe.csrf_token = data.csrf_token;
|
||||
}
|
||||
});
|
||||
|
||||
frappe.realtime.on("version-update", function() {
|
||||
var dialog = frappe.msgprint({
|
||||
message:__("The application has been updated to a new version, please refresh this page"),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
|
|||
set_description: function() {
|
||||
const { description } = this.df;
|
||||
const { time_zone } = frappe.sys_defaults;
|
||||
if (!frappe.datetime.is_timezone_same()) {
|
||||
if (!this.df.hide_timezone && !frappe.datetime.is_timezone_same()) {
|
||||
if (!description) {
|
||||
this.df.description = time_zone;
|
||||
} else if (!description.includes(time_zone)) {
|
||||
|
|
|
|||
|
|
@ -332,6 +332,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
|
|||
let docfield = frappe.meta.get_docfield(doctype, fieldname);
|
||||
let label = docfield ? docfield.label : frappe.model.unscrub(fieldname);
|
||||
|
||||
if (docfield && docfield.fieldtype === 'Check') {
|
||||
filter[3] = filter[3] ? __('Yes'): __('No');
|
||||
}
|
||||
|
||||
if (filter[3] && Array.isArray(filter[3]) && filter[3].length > 5) {
|
||||
filter[3] = filter[3].slice(0, 5);
|
||||
filter[3].push('...');
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ Quill.register(CustomColor, true);
|
|||
frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
|
||||
make_wrapper() {
|
||||
this._super();
|
||||
this.$wrapper.find(".like-disabled-input").addClass('ql-editor');
|
||||
},
|
||||
|
||||
make_input() {
|
||||
|
|
@ -199,6 +198,12 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
|
|||
|
||||
get_input_value() {
|
||||
let value = this.quill ? this.quill.root.innerHTML : '';
|
||||
// hack to retain space sequence.
|
||||
value = value.replace(/(\s)(\s)/g, ' ');
|
||||
|
||||
if (!$(value).find('.ql-editor').length) {
|
||||
value = `<div class="ql-editor read-mode">${value}</div>`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -120,9 +120,11 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
display_automatic_link_email() {
|
||||
let docinfo = this.frm.get_docinfo();
|
||||
|
||||
if (docinfo.document_email){
|
||||
if (docinfo.document_email) {
|
||||
let link = __("Send an email to {0} to link it here", [`<b><a class="timeline-email-import-link copy-to-clipboard">${docinfo.document_email}</a></b>`]);
|
||||
$('.timeline-email-import').html(link);
|
||||
const email_link = $('.timeline-email-import');
|
||||
email_link.removeClass('hide');
|
||||
email_link.html(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,12 +182,15 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
// append energy point logs
|
||||
timeline = timeline.concat(this.get_energy_point_logs());
|
||||
|
||||
// custom contents
|
||||
timeline = timeline.concat(this.get_additional_timeline_content());
|
||||
|
||||
// append milestones
|
||||
timeline = timeline.concat(this.get_milestones());
|
||||
|
||||
// sort
|
||||
timeline
|
||||
.filter(a => a.content)
|
||||
.filter(a => a.content || a.template)
|
||||
.sort((b, c) => me.compare_dates(b, c))
|
||||
.forEach(d => {
|
||||
d.frm = me.frm;
|
||||
|
|
@ -407,7 +412,10 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
c.original_content = c.content;
|
||||
c.content = frappe.utils.toggle_blockquote(c.content);
|
||||
}
|
||||
if(!frappe.utils.is_html(c.content)) {
|
||||
|
||||
if (c.template) {
|
||||
c.content_html = frappe.render_template(c.template, c.template_data);
|
||||
} else if (!frappe.utils.is_html(c.content)) {
|
||||
c.content_html = frappe.markdown(__(c.content));
|
||||
} else {
|
||||
c.content_html = c.content;
|
||||
|
|
@ -529,6 +537,10 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
return energy_point_logs;
|
||||
}
|
||||
|
||||
get_additional_timeline_content() {
|
||||
return this.frm.get_docinfo().additional_timeline_content || [];
|
||||
}
|
||||
|
||||
get_milestones() {
|
||||
let milestones = this.frm.get_docinfo().milestones;
|
||||
milestones.map(log => {
|
||||
|
|
|
|||
|
|
@ -842,6 +842,15 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.page.clear_primary_action();
|
||||
}
|
||||
|
||||
disable_form() {
|
||||
this.set_read_only();
|
||||
this.fields
|
||||
.forEach((field) => {
|
||||
this.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
});
|
||||
this.disable_save();
|
||||
}
|
||||
|
||||
handle_save_fail(btn, on_error) {
|
||||
$(btn).prop('disabled', false);
|
||||
if (on_error) {
|
||||
|
|
@ -1391,9 +1400,16 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
var docperms = frappe.perm.get_perm(this.doc.doctype);
|
||||
for (var i=0, l=docperms.length; i<l; i++) {
|
||||
var p = docperms[i];
|
||||
perm[p.permlevel || 0] = {read:1, print:1, cancel:1, email:1};
|
||||
perm[p.permlevel || 0] = {
|
||||
read: p.read,
|
||||
cancel: p.cancel,
|
||||
share: p.share,
|
||||
print: p.print,
|
||||
email: p.email
|
||||
};
|
||||
}
|
||||
this.perm = perm;
|
||||
this.toolbar.set_page_action();
|
||||
}
|
||||
|
||||
trigger(event, doctype, docname) {
|
||||
|
|
@ -1604,6 +1620,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
});
|
||||
|
||||
driver.defineSteps(steps);
|
||||
frappe.route.on('change', () => driver.reset());
|
||||
driver.start();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -228,7 +228,12 @@ frappe.form.formatters = {
|
|||
return frappe.form.formatters.Text(value);
|
||||
},
|
||||
TextEditor: function(value) {
|
||||
return frappe.form.formatters.Text(value);
|
||||
let formatted_value = frappe.form.formatters.Text(value);
|
||||
// to use ql-editor styles
|
||||
if (!$(formatted_value).find('.ql-editor').length) {
|
||||
formatted_value = `<div class="ql-editor read-mode">${formatted_value}</div>`;
|
||||
}
|
||||
return formatted_value;
|
||||
},
|
||||
Code: function(value) {
|
||||
return "<pre>" + (value==null ? "" : $("<div>").text(value).html()) + "</pre>"
|
||||
|
|
|
|||
|
|
@ -101,19 +101,25 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
columns[1] = [];
|
||||
columns[2] = [];
|
||||
|
||||
Object.keys(this.setters).forEach((setter, index) => {
|
||||
let df_prop = frappe.meta.docfield_map[this.doctype][setter];
|
||||
|
||||
// Index + 1 to start filling from index 1
|
||||
// Since Search is a standrd field already pushed
|
||||
columns[(index + 1) % 3].push({
|
||||
fieldtype: df_prop.fieldtype,
|
||||
label: df_prop.label,
|
||||
fieldname: setter,
|
||||
options: df_prop.options,
|
||||
default: this.setters[setter]
|
||||
if ($.isArray(this.setters)) {
|
||||
this.setters.forEach((setter, index) => {
|
||||
columns[(index + 1) % 3].push(setter);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.keys(this.setters).forEach((setter, index) => {
|
||||
let df_prop = frappe.meta.docfield_map[this.doctype][setter];
|
||||
|
||||
// Index + 1 to start filling from index 1
|
||||
// Since Search is a standrd field already pushed
|
||||
columns[(index + 1) % 3].push({
|
||||
fieldtype: df_prop.fieldtype,
|
||||
label: df_prop.label,
|
||||
fieldname: setter,
|
||||
options: df_prop.options,
|
||||
default: this.setters[setter]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal
|
||||
if (Object.seal) {
|
||||
|
|
@ -217,7 +223,13 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
let contents = ``;
|
||||
let columns = ["name"];
|
||||
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
if ($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
columns.push(df.fieldname);
|
||||
}
|
||||
} else {
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
}
|
||||
|
||||
columns.forEach(function (column) {
|
||||
contents += `<div class="list-item__content ellipsis">
|
||||
|
|
@ -290,16 +302,24 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
let filters = this.get_query ? this.get_query().filters : {} || {};
|
||||
let filter_fields = [];
|
||||
|
||||
Object.keys(this.setters).forEach(function (setter) {
|
||||
var value = me.dialog.fields_dict[setter].get_value();
|
||||
if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
|
||||
filters[setter] = ["like", "%" + value + "%"];
|
||||
} else {
|
||||
filters[setter] = value || undefined;
|
||||
me.args[setter] = filters[setter];
|
||||
filter_fields.push(setter);
|
||||
if ($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
|
||||
me.args[df.fieldname] = filters[df.fieldname];
|
||||
filter_fields.push(df.fieldname);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Object.keys(this.setters).forEach(function (setter) {
|
||||
var value = me.dialog.fields_dict[setter].get_value();
|
||||
if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
|
||||
filters[setter] = ["like", "%" + value + "%"];
|
||||
} else {
|
||||
filters[setter] = value || undefined;
|
||||
me.args[setter] = filters[setter];
|
||||
filter_fields.push(setter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let filter_group = this.get_custom_filters();
|
||||
Object.assign(filters, filter_group);
|
||||
|
|
|
|||
|
|
@ -240,13 +240,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
var me = this;
|
||||
var data = this.dialog.get_values(true);
|
||||
$.each(data, function(key, value) {
|
||||
if(key==='__newname') {
|
||||
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;
|
||||
|
|
@ -282,7 +277,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
field.doctype = me.doc.doctype;
|
||||
field.docname = me.doc.name;
|
||||
|
||||
if(!is_null(me.doc[fieldname])) {
|
||||
if (!is_null(me.doc[fieldname])) {
|
||||
field.set_input(me.doc[fieldname]);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@
|
|||
{% } %}
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="timeline-email-import text-muted small">
|
||||
|
||||
</div>
|
||||
<div class="timeline-email-import text-muted small hide"></div>
|
||||
<div class="timeline-items">
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<div class="media timeline-item {% if (data.user_content) { %} user-content {% } else { %} notification-content {% } %} {{ data.color || "" }}"
|
||||
<div class="media timeline-item
|
||||
{% if (data.user_content || data.template) { %} user-content {% } else { %} notification-content {% } %}
|
||||
{% if (data.template) { %} show-indicator {% }%} {{ data.color || "" }}"
|
||||
data-doctype="{{ data.doctype }}" data-name="{{ data.name }}" data-communication-type = "{{ data.communication_type }}">
|
||||
{% if (data.user_content) { %}
|
||||
<span class="pull-left avatar avatar-medium hidden-xs" style="margin-top: 1px">
|
||||
|
|
@ -186,7 +188,7 @@
|
|||
{% } %}
|
||||
{%= __("Liked by {0}", [data.fullname]) %}
|
||||
</span>
|
||||
{% } else if (data.comment_type == "Energy Points") { %}
|
||||
{% } else if (data.comment_type == "Energy Points" || data.template) { %}
|
||||
{{ data.content_html }}
|
||||
{% } else { %}
|
||||
<b title="{{ data.comment_by }}">{%= data.fullname %}</b>
|
||||
|
|
@ -200,8 +202,11 @@
|
|||
</a>
|
||||
{% } %}
|
||||
{% } %}
|
||||
<span class="text-muted commented-on" style="font-weight: normal;">
|
||||
– {%= data.comment_on %}</span>
|
||||
{% if (!data.template) { %}
|
||||
<span class="text-muted commented-on" style="font-weight: normal;">
|
||||
– {%= data.comment_on %}
|
||||
</span>
|
||||
{% } %}
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -109,9 +109,10 @@ frappe.views.BaseList = class BaseList {
|
|||
this.fields = this.fields.uniqBy(f => f[0] + f[1]);
|
||||
}
|
||||
|
||||
_add_field(fieldname) {
|
||||
_add_field(fieldname, doctype) {
|
||||
if (!fieldname) return;
|
||||
let doctype = this.doctype;
|
||||
|
||||
if (!doctype) doctype = this.doctype;
|
||||
|
||||
if (typeof fieldname === 'object') {
|
||||
// df is passed
|
||||
|
|
@ -120,6 +121,8 @@ frappe.views.BaseList = class BaseList {
|
|||
doctype = df.parent;
|
||||
}
|
||||
|
||||
if (!this.fields) this.fields = [];
|
||||
|
||||
const is_valid_field = frappe.model.std_fields_list.includes(fieldname)
|
||||
|| frappe.meta.has_field(doctype, fieldname)
|
||||
|| fieldname === '_seen';
|
||||
|
|
|
|||
|
|
@ -528,6 +528,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
get_header_html() {
|
||||
if (!this.columns) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subject_field = this.columns[0].df;
|
||||
let subject_html = `
|
||||
<input class="level-item list-check-all hidden-xs" type="checkbox" title="${__("Select All")}">
|
||||
|
|
@ -760,26 +764,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
let current_count = this.data.length;
|
||||
let count_without_children = this.data.uniqBy(d => d.name).length;
|
||||
|
||||
const filters = this.get_filters_for_args();
|
||||
const with_child_table_filter = filters.some(filter => {
|
||||
return filter[0] !== this.doctype;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
// cannot break this line as it adds extra \n's and \t's which breaks the query
|
||||
`count(${with_child_table_filter ? 'distinct': ''}${frappe.model.get_full_column_name('name', this.doctype)}) AS total_count`
|
||||
];
|
||||
|
||||
return frappe.call({
|
||||
type: 'GET',
|
||||
method: this.method,
|
||||
args: {
|
||||
doctype: this.doctype,
|
||||
filters,
|
||||
fields,
|
||||
}
|
||||
}).then(r => {
|
||||
this.total_count = r.message.values[0][0] || current_count;
|
||||
return frappe.db.count(this.doctype, {
|
||||
filters: this.get_filters_for_args()
|
||||
}).then(total_count => {
|
||||
this.total_count = total_count || current_count;
|
||||
let str = __('{0} of {1}', [current_count, this.total_count]);
|
||||
if (count_without_children !== current_count) {
|
||||
str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]);
|
||||
|
|
@ -800,6 +788,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
return '#Form/' + this.doctype + '/' + docname;
|
||||
}
|
||||
|
||||
get_seen_class(doc) {
|
||||
return JSON.parse(doc._seen || '[]').includes(frappe.session.user)
|
||||
? ''
|
||||
: 'bold';
|
||||
}
|
||||
|
||||
get_subject_html(doc) {
|
||||
let user = frappe.session.user;
|
||||
let subject_field = this.columns[0].df;
|
||||
|
|
@ -811,8 +805,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
let heart_class = liked_by.includes(user) ?
|
||||
'liked-by' : 'text-extra-muted not-liked';
|
||||
|
||||
const seen = JSON.parse(doc._seen || '[]')
|
||||
.includes(user) ? '' : 'bold';
|
||||
const seen = this.get_seen_class(doc);
|
||||
|
||||
let subject_html = `
|
||||
<input class="level-item list-row-checkbox hidden-xs" type="checkbox" data-name="${escape(doc.name)}">
|
||||
|
|
@ -1162,7 +1155,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
});
|
||||
this.toggle_result_area();
|
||||
this.render_list();
|
||||
if (this.$checks.length) {
|
||||
if (this.$checks && this.$checks.length) {
|
||||
this.set_rows_as_checked();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ frappe.request.call = function(opts) {
|
|||
message: __('The resource you are looking for is not available')});
|
||||
},
|
||||
403: function(xhr) {
|
||||
if (frappe.get_cookie('sid')==='Guest') {
|
||||
if (frappe.session.user === 'Guest') {
|
||||
// session expired
|
||||
frappe.app.handle_session_expired();
|
||||
}
|
||||
|
|
@ -321,7 +321,7 @@ frappe.request.cleanup = function(opts, r) {
|
|||
if(r) {
|
||||
|
||||
// session expired? - Guest has no business here!
|
||||
if(r.session_expired || frappe.get_cookie("sid")==="Guest") {
|
||||
if (r.session_expired || frappe.session.user === "Guest") {
|
||||
frappe.app.handle_session_expired();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
</div>
|
||||
<div class="col-sm-4 form-group">
|
||||
<div class="filter-field"></div>
|
||||
<div class="text-muted small filter-description"></div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="filter-actions">
|
||||
|
|
|
|||
|
|
@ -13,26 +13,26 @@ frappe.ui.Filter = class {
|
|||
|
||||
set_conditions() {
|
||||
this.conditions = [
|
||||
["=", __("Equals")],
|
||||
["!=", __("Not Equals")],
|
||||
["like", __("Like")],
|
||||
["not like", __("Not Like")],
|
||||
["in", __("In")],
|
||||
["not in", __("Not In")],
|
||||
["is", __("Is")],
|
||||
[">", ">"],
|
||||
["<", "<"],
|
||||
[">=", ">="],
|
||||
["<=", "<="],
|
||||
["Between", __("Between")],
|
||||
["Timespan", __("Timespan")],
|
||||
['=', __('Equals')],
|
||||
['!=', __('Not Equals')],
|
||||
['like', __('Like')],
|
||||
['not like', __('Not Like')],
|
||||
['in', __('In')],
|
||||
['not in', __('Not In')],
|
||||
['is', __('Is')],
|
||||
['>', '>'],
|
||||
['<', '<'],
|
||||
['>=', '>='],
|
||||
['<=', '<='],
|
||||
['Between', __('Between')],
|
||||
['Timespan', __('Timespan')],
|
||||
];
|
||||
|
||||
this.nested_set_conditions = [
|
||||
["descendants of", __("Descendants Of")],
|
||||
["not descendants of", __("Not Descendants Of")],
|
||||
["ancestors of", __("Ancestors Of")],
|
||||
["not ancestors of", __("Not Ancestors Of")],
|
||||
['descendants of', __('Descendants Of')],
|
||||
['not descendants of', __('Not Descendants Of')],
|
||||
['ancestors of', __('Ancestors Of')],
|
||||
['not ancestors of', __('Not Ancestors Of')],
|
||||
];
|
||||
|
||||
this.conditions.push(...this.nested_set_conditions);
|
||||
|
|
@ -42,10 +42,10 @@ frappe.ui.Filter = class {
|
|||
Datetime: ['like', 'not like'],
|
||||
Data: ['Between', 'Timespan'],
|
||||
Select: ['like', 'not like', 'Between', 'Timespan'],
|
||||
Link: ["Between", 'Timespan', '>', '<', '>=', '<='],
|
||||
Currency: ["Between", 'Timespan'],
|
||||
Color: ["Between", 'Timespan'],
|
||||
Check: this.conditions.map(c => c[0]).filter(c => c !== '=')
|
||||
Link: ['Between', 'Timespan', '>', '<', '>=', '<='],
|
||||
Currency: ['Between', 'Timespan'],
|
||||
Color: ['Between', 'Timespan'],
|
||||
Check: this.conditions.map((c) => c[0]).filter((c) => c !== '='),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +65,11 @@ frappe.ui.Filter = class {
|
|||
}
|
||||
|
||||
make() {
|
||||
this.filter_edit_area = $(frappe.render_template("edit_filter", {
|
||||
conditions: this.conditions
|
||||
}))
|
||||
.appendTo(this.parent.find('.filter-edit-area'));
|
||||
this.filter_edit_area = $(
|
||||
frappe.render_template('edit_filter', {
|
||||
conditions: this.conditions,
|
||||
})
|
||||
).appendTo(this.parent.find('.filter-edit-area'));
|
||||
|
||||
this.make_select();
|
||||
this.set_events();
|
||||
|
|
@ -82,41 +83,51 @@ frappe.ui.Filter = class {
|
|||
filter_fields: this.filter_fields,
|
||||
select: (doctype, fieldname) => {
|
||||
this.set_field(doctype, fieldname);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if(this.fieldname) {
|
||||
if (this.fieldname) {
|
||||
this.fieldselect.set_value(this.doctype, this.fieldname);
|
||||
}
|
||||
}
|
||||
|
||||
set_events() {
|
||||
this.filter_edit_area.find("a.remove-filter").on("click", () => {
|
||||
this.filter_edit_area.find('a.remove-filter').on('click', () => {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
this.filter_edit_area.find(".set-filter-and-run").on("click", () => {
|
||||
this.filter_edit_area.removeClass("new-filter");
|
||||
this.filter_edit_area.find('.set-filter-and-run').on('click', () => {
|
||||
this.filter_edit_area.removeClass('new-filter');
|
||||
this.on_change();
|
||||
this.update_filter_tag();
|
||||
});
|
||||
|
||||
this.filter_edit_area.find('.condition').change(() => {
|
||||
if(!this.field) return;
|
||||
if (!this.field) return;
|
||||
|
||||
let condition = this.get_condition();
|
||||
let fieldtype = null;
|
||||
|
||||
if(["in", "like", "not in", "not like"].includes(condition)) {
|
||||
if (['in', 'like', 'not in', 'not like'].includes(condition)) {
|
||||
fieldtype = 'Data';
|
||||
this.add_condition_help(condition);
|
||||
} else {
|
||||
this.filter_edit_area.find('.filter-description').empty();
|
||||
}
|
||||
|
||||
if (['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && ["in", "not in"].includes(condition)) {
|
||||
if (
|
||||
['Select', 'MultiSelect'].includes(this.field.df.fieldtype) &&
|
||||
['in', 'not in'].includes(condition)
|
||||
) {
|
||||
fieldtype = 'MultiSelect';
|
||||
}
|
||||
|
||||
this.set_field(this.field.df.parent, this.field.df.fieldname, fieldtype, condition);
|
||||
this.set_field(
|
||||
this.field.df.parent,
|
||||
this.field.df.fieldname,
|
||||
fieldtype,
|
||||
condition
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -129,12 +140,12 @@ frappe.ui.Filter = class {
|
|||
setup_state(is_new) {
|
||||
let promise = Promise.resolve();
|
||||
if (is_new) {
|
||||
this.filter_edit_area.addClass("new-filter");
|
||||
this.filter_edit_area.addClass('new-filter');
|
||||
} else {
|
||||
promise = this.update_filter_tag();
|
||||
}
|
||||
|
||||
if(this.hidden) {
|
||||
if (this.hidden) {
|
||||
promise.then(() => this.$filter_tag.hide());
|
||||
}
|
||||
}
|
||||
|
|
@ -164,13 +175,13 @@ frappe.ui.Filter = class {
|
|||
set_values(doctype, fieldname, condition, value) {
|
||||
// presents given (could be via tags!)
|
||||
if (this.set_field(doctype, fieldname) === false) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.field.df.original_type==='Check') {
|
||||
value = (value==1) ? 'Yes' : 'No';
|
||||
if (this.field.df.original_type === 'Check') {
|
||||
value = value == 1 ? 'Yes' : 'No';
|
||||
}
|
||||
if(condition) this.set_condition(condition, true);
|
||||
if (condition) this.set_condition(condition, true);
|
||||
|
||||
// set value can be asynchronous, so update_filter_tag should happen after field is set
|
||||
this._filter_value_set = Promise.resolve();
|
||||
|
|
@ -190,11 +201,13 @@ frappe.ui.Filter = class {
|
|||
set_field(doctype, fieldname, fieldtype, condition) {
|
||||
// set in fieldname (again)
|
||||
let cur = {};
|
||||
if(this.field) for(let k in this.field.df) cur[k] = this.field.df[k];
|
||||
if (this.field) for (let k in this.field.df) cur[k] = this.field.df[k];
|
||||
|
||||
let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname];
|
||||
let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[
|
||||
fieldname
|
||||
];
|
||||
|
||||
if(!original_docfield) {
|
||||
if (!original_docfield) {
|
||||
console.warn(`Field ${fieldname} is not selectable.`);
|
||||
this.remove();
|
||||
return false;
|
||||
|
|
@ -214,8 +227,13 @@ frappe.ui.Filter = class {
|
|||
|
||||
// called when condition is changed,
|
||||
// don't change if all is well
|
||||
if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype &&
|
||||
df.parent == cur.parent && df.options == cur.options) {
|
||||
if (
|
||||
this.field &&
|
||||
cur.fieldname == fieldname &&
|
||||
df.fieldtype == cur.fieldtype &&
|
||||
df.parent == cur.parent &&
|
||||
df.options == cur.options
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -223,20 +241,25 @@ frappe.ui.Filter = class {
|
|||
this.fieldselect.selected_doctype = doctype;
|
||||
this.fieldselect.selected_fieldname = fieldname;
|
||||
|
||||
if (this.filters_config && this.filters_config[condition]
|
||||
&& this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)) {
|
||||
if (
|
||||
this.filters_config &&
|
||||
this.filters_config[condition] &&
|
||||
this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)
|
||||
) {
|
||||
let args = {};
|
||||
if (this.filters_config[condition].depends_on) {
|
||||
const field_name = this.filters_config[condition].depends_on;
|
||||
const filter_value = this.base_list.get_filter_value(field_name);
|
||||
args[field_name] = filter_value;
|
||||
}
|
||||
frappe.xcall(this.filters_config[condition].get_field, args).then(field => {
|
||||
df.fieldtype = field.fieldtype;
|
||||
df.options = field.options;
|
||||
df.fieldname = fieldname;
|
||||
this.make_field(df, cur.fieldtype);
|
||||
});
|
||||
frappe
|
||||
.xcall(this.filters_config[condition].get_field, args)
|
||||
.then(field => {
|
||||
df.fieldtype = field.fieldtype;
|
||||
df.options = field.options;
|
||||
df.fieldname = fieldname;
|
||||
this.make_field(df, cur.fieldtype);
|
||||
});
|
||||
} else {
|
||||
this.make_field(df, cur.fieldtype);
|
||||
}
|
||||
|
|
@ -255,16 +278,18 @@ frappe.ui.Filter = class {
|
|||
f.refresh();
|
||||
|
||||
this.field = f;
|
||||
if(old_text && f.fieldtype===old_fieldtype) {
|
||||
if (old_text && f.fieldtype === old_fieldtype) {
|
||||
this.field.set_value(old_text);
|
||||
}
|
||||
|
||||
// run on enter
|
||||
$(this.field.wrapper).find(':input').keydown(e => {
|
||||
if(e.which==13 && this.field.df.fieldtype !== 'MultiSelect') {
|
||||
this.on_change();
|
||||
}
|
||||
});
|
||||
$(this.field.wrapper)
|
||||
.find(':input')
|
||||
.keydown(e => {
|
||||
if (e.which == 13 && this.field.df.fieldtype !== 'MultiSelect') {
|
||||
this.on_change();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_value() {
|
||||
|
|
@ -273,7 +298,7 @@ frappe.ui.Filter = class {
|
|||
this.field.df.fieldname,
|
||||
this.get_condition(),
|
||||
this.get_selected_value(),
|
||||
this.hidden
|
||||
this.hidden,
|
||||
];
|
||||
}
|
||||
get_selected_value() {
|
||||
|
|
@ -284,90 +309,101 @@ frappe.ui.Filter = class {
|
|||
return this.filter_edit_area.find('.condition').val();
|
||||
}
|
||||
|
||||
set_condition(condition, trigger_change=false) {
|
||||
set_condition(condition, trigger_change = false) {
|
||||
let $condition_field = this.filter_edit_area.find('.condition');
|
||||
$condition_field.val(condition);
|
||||
if(trigger_change) $condition_field.change();
|
||||
|
||||
if (trigger_change) $condition_field.change();
|
||||
}
|
||||
|
||||
make_tag() {
|
||||
if (!this.field) return;
|
||||
this.$filter_tag = this.get_filter_tag_element()
|
||||
.insertAfter(this.parent.find(".active-tag-filters .clear-filters"));
|
||||
this.$filter_tag = this.get_filter_tag_element().insertAfter(
|
||||
this.parent.find('.active-tag-filters .clear-filters')
|
||||
);
|
||||
this.set_filter_button_text();
|
||||
this.bind_tag();
|
||||
}
|
||||
|
||||
bind_tag() {
|
||||
this.$filter_tag.find(".remove-filter").on("click", this.remove.bind(this));
|
||||
this.$filter_tag.find('.remove-filter').on('click', this.remove.bind(this));
|
||||
|
||||
let filter_button = this.$filter_tag.find(".toggle-filter");
|
||||
filter_button.on("click", () => {
|
||||
filter_button.closest('.tag-filters-area').find('.filter-edit-area').show();
|
||||
let filter_button = this.$filter_tag.find('.toggle-filter');
|
||||
filter_button.on('click', () => {
|
||||
filter_button
|
||||
.closest('.tag-filters-area')
|
||||
.find('.filter-edit-area')
|
||||
.show();
|
||||
this.filter_edit_area.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
set_filter_button_text() {
|
||||
this.$filter_tag.find(".toggle-filter").html(this.get_filter_button_text());
|
||||
this.$filter_tag.find('.toggle-filter').html(this.get_filter_button_text());
|
||||
}
|
||||
|
||||
get_filter_button_text() {
|
||||
let value = this.utils.get_formatted_value(this.field, this.get_selected_value());
|
||||
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(value)}`;
|
||||
let value = this.utils.get_formatted_value(
|
||||
this.field,
|
||||
this.get_selected_value()
|
||||
);
|
||||
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(
|
||||
value
|
||||
)}`;
|
||||
}
|
||||
|
||||
get_filter_tag_element() {
|
||||
return $(`<div class="filter-tag btn-group">
|
||||
<button class="btn btn-default btn-xs toggle-filter"
|
||||
title="${ __("Edit Filter") }">
|
||||
title="${__('Edit Filter')}">
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs remove-filter"
|
||||
title="${ __("Remove Filter") }">
|
||||
title="${__('Remove Filter')}">
|
||||
<i class="fa fa-remove text-muted"></i>
|
||||
</button>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
add_condition_help(condition) {
|
||||
let $desc = this.field.desc_area;
|
||||
if(!$desc) {
|
||||
$desc = $('<div class="text-muted small">').appendTo(this.field.wrapper);
|
||||
}
|
||||
// set description
|
||||
$desc.html((in_list(["in", "not in"], condition)==="in"
|
||||
? __("values separated by commas")
|
||||
: __("use % as wildcard"))+'</div>');
|
||||
const description = ['in', 'not in'].includes(condition)
|
||||
? __('values separated by commas')
|
||||
: __('use % as wildcard');
|
||||
|
||||
this.filter_edit_area.find('.filter-description').html(description);
|
||||
}
|
||||
|
||||
hide_invalid_conditions(fieldtype, original_type) {
|
||||
let invalid_conditions = this.invalid_condition_map[original_type]
|
||||
|| this.invalid_condition_map[fieldtype] || [];
|
||||
let invalid_conditions =
|
||||
this.invalid_condition_map[original_type] ||
|
||||
this.invalid_condition_map[fieldtype] ||
|
||||
[];
|
||||
|
||||
for (let condition of this.conditions) {
|
||||
this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(
|
||||
!invalid_conditions.includes(condition[0])
|
||||
);
|
||||
this.filter_edit_area
|
||||
.find(`.condition option[value="${condition[0]}"]`)
|
||||
.toggle(!invalid_conditions.includes(condition[0]));
|
||||
}
|
||||
}
|
||||
|
||||
toggle_nested_set_conditions(df) {
|
||||
let show_condition = df.fieldtype === "Link" && frappe.boot.nested_set_doctypes.includes(df.options);
|
||||
this.nested_set_conditions.forEach(condition => {
|
||||
this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(show_condition);
|
||||
let show_condition =
|
||||
df.fieldtype === 'Link' &&
|
||||
frappe.boot.nested_set_doctypes.includes(df.options);
|
||||
this.nested_set_conditions.forEach((condition) => {
|
||||
this.filter_edit_area
|
||||
.find(`.condition option[value="${condition[0]}"]`)
|
||||
.toggle(show_condition);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.filter_utils = {
|
||||
get_formatted_value(field, value) {
|
||||
if(field.df.fieldname==="docstatus") {
|
||||
value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
|
||||
} else if(field.df.original_type==="Check") {
|
||||
value = {0:"No", 1:"Yes"}[cint(value)];
|
||||
if (field.df.fieldname === 'docstatus') {
|
||||
value = { 0: 'Draft', 1: 'Submitted', 2: 'Cancelled' }[value] || value;
|
||||
} else if (field.df.original_type === 'Check') {
|
||||
value = { 0: 'No', 1: 'Yes' }[cint(value)];
|
||||
}
|
||||
return frappe.format(value, field.df, {only_value: 1});
|
||||
return frappe.format(value, field.df, { only_value: 1 });
|
||||
},
|
||||
|
||||
get_selected_value(field, condition) {
|
||||
|
|
@ -382,7 +418,7 @@ frappe.ui.filter_utils = {
|
|||
}
|
||||
|
||||
if (field.df.original_type == 'Check') {
|
||||
val = (val=='Yes' ? 1 :0);
|
||||
val = val == 'Yes' ? 1 : 0;
|
||||
}
|
||||
|
||||
if (condition.indexOf('like', 'not like') !== -1) {
|
||||
|
|
@ -390,12 +426,13 @@ frappe.ui.filter_utils = {
|
|||
if (val && !(val.startsWith('%') || val.endsWith('%'))) {
|
||||
val = '%' + val + '%';
|
||||
}
|
||||
} else if (in_list(["in", "not in"], condition)) {
|
||||
} else if (in_list(['in', 'not in'], condition)) {
|
||||
if (val) {
|
||||
val = val.split(',').map(v => strip(v));
|
||||
val = val.split(',').map((v) => strip(v));
|
||||
}
|
||||
} if (val === '%') {
|
||||
val = "";
|
||||
}
|
||||
if (val === '%') {
|
||||
val = '';
|
||||
}
|
||||
|
||||
return val;
|
||||
|
|
@ -404,7 +441,7 @@ frappe.ui.filter_utils = {
|
|||
get_default_condition(df) {
|
||||
if (df.fieldtype == 'Data') {
|
||||
return 'like';
|
||||
} else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){
|
||||
} else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') {
|
||||
return 'Between';
|
||||
} else {
|
||||
return '=';
|
||||
|
|
@ -413,44 +450,73 @@ frappe.ui.filter_utils = {
|
|||
|
||||
set_fieldtype(df, fieldtype, condition) {
|
||||
// reset
|
||||
if(df.original_type)
|
||||
df.fieldtype = df.original_type;
|
||||
else
|
||||
df.original_type = df.fieldtype;
|
||||
if (df.original_type) df.fieldtype = df.original_type;
|
||||
else df.original_type = df.fieldtype;
|
||||
|
||||
df.description = ''; df.reqd = 0;
|
||||
df.description = '';
|
||||
df.reqd = 0;
|
||||
df.ignore_link_validation = true;
|
||||
|
||||
// given
|
||||
if(fieldtype) {
|
||||
if (fieldtype) {
|
||||
df.fieldtype = fieldtype;
|
||||
return;
|
||||
}
|
||||
|
||||
// scrub
|
||||
if(df.fieldname=="docstatus") {
|
||||
df.fieldtype="Select",
|
||||
df.options=[
|
||||
{value:0, label:__("Draft")},
|
||||
{value:1, label:__("Submitted")},
|
||||
{value:2, label:__("Cancelled")}
|
||||
if (df.fieldname == 'docstatus') {
|
||||
df.fieldtype = 'Select',
|
||||
df.options = [
|
||||
{ value: 0, label: __('Draft') },
|
||||
{ value: 1, label: __('Submitted') },
|
||||
{ value: 2, label: __('Cancelled') },
|
||||
];
|
||||
} else if(df.fieldtype=='Check') {
|
||||
df.fieldtype='Select';
|
||||
df.options='No\nYes';
|
||||
} else if(['Text','Small Text','Text Editor','Code','Tag','Comments',
|
||||
'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) {
|
||||
} else if (df.fieldtype == 'Check') {
|
||||
df.fieldtype = 'Select';
|
||||
df.options = 'No\nYes';
|
||||
} else if (
|
||||
[
|
||||
'Text',
|
||||
'Small Text',
|
||||
'Text Editor',
|
||||
'Code',
|
||||
'Tag',
|
||||
'Comments',
|
||||
'Dynamic Link',
|
||||
'Read Only',
|
||||
'Assign',
|
||||
].indexOf(df.fieldtype) != -1
|
||||
) {
|
||||
df.fieldtype = 'Data';
|
||||
} else if(df.fieldtype=='Link' && ['=', '!=', 'descendants of', 'ancestors of', 'not descendants of', 'not ancestors of'].indexOf(condition)==-1) {
|
||||
} else if (
|
||||
df.fieldtype == 'Link' &&
|
||||
[
|
||||
'=',
|
||||
'!=',
|
||||
'descendants of',
|
||||
'ancestors of',
|
||||
'not descendants of',
|
||||
'not ancestors of',
|
||||
].indexOf(condition) == -1
|
||||
) {
|
||||
df.fieldtype = 'Data';
|
||||
}
|
||||
if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") {
|
||||
if (
|
||||
df.fieldtype === 'Data' &&
|
||||
(df.options || '').toLowerCase() === 'email'
|
||||
) {
|
||||
df.options = null;
|
||||
}
|
||||
if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){
|
||||
if (
|
||||
condition == 'Between' &&
|
||||
(df.fieldtype == 'Date' || df.fieldtype == 'Datetime')
|
||||
) {
|
||||
df.fieldtype = 'DateRange';
|
||||
}
|
||||
if (condition == 'Timespan' && ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)) {
|
||||
if (
|
||||
condition == 'Timespan' &&
|
||||
['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)
|
||||
) {
|
||||
df.fieldtype = 'Select';
|
||||
df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']);
|
||||
}
|
||||
|
|
@ -466,15 +532,15 @@ frappe.ui.filter_utils = {
|
|||
|
||||
get_timespan_options(periods) {
|
||||
const period_map = {
|
||||
'Last': ['Week', 'Month', 'Quarter', '6 months', 'Year'],
|
||||
'Today': null,
|
||||
'This': ['Week', 'Month', 'Quarter', 'Year'],
|
||||
'Next': ['Week', 'Month', 'Quarter', '6 months', 'Year']
|
||||
Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
|
||||
Today: null,
|
||||
This: ['Week', 'Month', 'Quarter', 'Year'],
|
||||
Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
|
||||
};
|
||||
let options = [];
|
||||
periods.forEach(period => {
|
||||
periods.forEach((period) => {
|
||||
if (period_map[period]) {
|
||||
period_map[period].forEach(p => {
|
||||
period_map[period].forEach((p) => {
|
||||
options.push({
|
||||
label: __(`{0} {1}`, [period, p]),
|
||||
value: `${period.toLowerCase()} ${p.toLowerCase()}`,
|
||||
|
|
@ -488,5 +554,5 @@ frappe.ui.filter_utils = {
|
|||
}
|
||||
});
|
||||
return options;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ frappe.ui.Notifications = class Notifications {
|
|||
let mark_read_action = field.read ? '': 'data-action="mark_as_read"';
|
||||
let message = field.subject;
|
||||
let title = message.match(/<b class="subject-title">(.*?)<\/b>/);
|
||||
message = title ? message.replace(title[1], frappe.ellipsis(title[1], 100)): message;
|
||||
message = title ? message.replace(title[1], frappe.ellipsis(strip_html(title[1]), 100)) : message;
|
||||
let message_html = `<div class="message">${message}</div>`;
|
||||
let user = field.from_user;
|
||||
let user_avatar = frappe.avatar(user, 'avatar-small user-avatar');
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ frappe.search.AwesomeBar = Class.extend({
|
|||
}
|
||||
|
||||
this.options.push({
|
||||
label: __("Search for '{0}'", [txt.bold()]),
|
||||
label: __("Search for '{0}'", [frappe.utils.xss_sanitise(txt).bold()]),
|
||||
value: __("Search for '{0}'", [txt]),
|
||||
match: txt,
|
||||
index: 100,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ frappe.dashboard_utils = {
|
|||
frappe.dom.eval(config);
|
||||
return frappe.dashboards.chart_sources[chart.source].filters;
|
||||
});
|
||||
} else if (chart.chart_type === 'Report') {
|
||||
} else if (chart.chart_type === 'Report' && chart.report_name) {
|
||||
return frappe.report_utils.get_report_filters(chart.report_name).then(filters => {
|
||||
return filters;
|
||||
});
|
||||
|
|
@ -97,6 +97,108 @@ frappe.dashboard_utils = {
|
|||
|
||||
get_year(date_str) {
|
||||
return date_str.substring(0, date_str.indexOf('-'));
|
||||
},
|
||||
|
||||
remove_common_static_filter_values(static_filters, dynamic_filters) {
|
||||
if (dynamic_filters) {
|
||||
if ($.isArray(static_filters)) {
|
||||
static_filters = static_filters.filter(static_filter => {
|
||||
for (let dynamic_filter of dynamic_filters) {
|
||||
if (static_filter[0] == dynamic_filter[0]
|
||||
&& static_filter[1] == dynamic_filter[1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
for (let key of Object.keys(dynamic_filters)) {
|
||||
delete static_filters[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return static_filters;
|
||||
},
|
||||
|
||||
get_fields_for_dynamic_filter_dialog(is_document_type, filters, dynamic_filters) {
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'description',
|
||||
options:
|
||||
`<div>
|
||||
<p>Set dynamic filter values in JavaScript for the required fields here.
|
||||
</p>
|
||||
<p>Ex:
|
||||
<code>frappe.defaults.get_user_default("Company")</code>
|
||||
</p>
|
||||
</div>`
|
||||
}
|
||||
];
|
||||
|
||||
if (is_document_type) {
|
||||
if (dynamic_filters) {
|
||||
filters = [...filters, ...dynamic_filters];
|
||||
}
|
||||
filters.forEach(f => {
|
||||
for (let field of fields) {
|
||||
if (field.fieldname == f[0] + ':' + f[1]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (f[2] == '=') {
|
||||
fields.push({
|
||||
label: `${f[1]} (${f[0]})`,
|
||||
fieldname: f[0] + ':' + f[1],
|
||||
fieldtype: 'Data',
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
filters = {...dynamic_filters, ...filters};
|
||||
for (let key of Object.keys(filters)) {
|
||||
fields.push({
|
||||
label: key,
|
||||
fieldname: key,
|
||||
fieldtype: 'Data',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
},
|
||||
|
||||
get_all_filters(doc) {
|
||||
let filters = JSON.parse(doc.filters_json || "null");
|
||||
let dynamic_filters = JSON.parse(doc.dynamic_filters_json || "null");
|
||||
|
||||
if (!dynamic_filters) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
if ($.isArray(dynamic_filters)) {
|
||||
dynamic_filters.forEach(f => {
|
||||
try {
|
||||
f[3] = eval(f[3]);
|
||||
} catch (e) {
|
||||
frappe.throw(__(`Invalid expression set in filter ${f[1]} (${f[0]})`));
|
||||
}
|
||||
});
|
||||
filters = [...filters, ...dynamic_filters];
|
||||
} else {
|
||||
for (let key of Object.keys(dynamic_filters)) {
|
||||
try {
|
||||
const val = eval(dynamic_filters[key]);
|
||||
dynamic_filters[key] = val;
|
||||
} catch (e) {
|
||||
frappe.throw(__(`Invalid expression set in filter ${key}`));
|
||||
}
|
||||
}
|
||||
Object.assign(filters, dynamic_filters);
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue