Merge branch 'develop' of https://github.com/frappe/frappe into table_multiselect_filter
This commit is contained in:
commit
9e75cd246b
227 changed files with 6334 additions and 2049 deletions
26
.travis.yml
26
.travis.yml
|
|
@ -25,6 +25,7 @@ cache:
|
|||
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
|
||||
- ~/.cache
|
||||
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- name: "Python 3.7 MariaDB"
|
||||
|
|
@ -46,7 +47,30 @@ matrix:
|
|||
script: bench --site test_site run-ui-tests frappe --headless
|
||||
|
||||
before_install:
|
||||
# install wkhtmltopdf
|
||||
# do we really want to run travis? check which files are changed and if git doesnt face any fatal errors
|
||||
- |
|
||||
FILES_CHANGED=$( git diff --name-only $TRAVIS_COMMIT_RANGE 2>&1 )
|
||||
|
||||
if [[ $FILES_CHANGED != *"fatal"* ]]; then
|
||||
ONLY_DOCS_CHANGES=$( echo $FILES_CHANGED | grep -qvE '\.(md|png|jpg|jpeg)$|^.github|LICENSE' ; echo $? )
|
||||
ONLY_JS_CHANGES=$( echo $FILES_CHANGED | grep -qvE '\.js$' ; echo $? )
|
||||
ONLY_PY_CHANGES=$( echo $FILES_CHANGED | grep -qvE '\.py$' ; echo $? )
|
||||
|
||||
if [[ $ONLY_DOCS_CHANGES == "1" ]]; then
|
||||
echo "Only docs were updated, stopping build process.";
|
||||
exit;
|
||||
fi
|
||||
if [[ $ONLY_JS_CHANGES == "1" && $TYPE == "server" ]]; then
|
||||
echo "Only JavaScript code was updated; Stopping Python build process.";
|
||||
exit;
|
||||
fi
|
||||
if [[ $ONLY_PY_CHANGES == "1" && $TYPE == "ui" ]]; then
|
||||
echo "Only Python code was updated, stopping Cypress build process.";
|
||||
exit;
|
||||
fi
|
||||
fi
|
||||
|
||||
# install wkhtmltopdf
|
||||
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div align="center">
|
||||
<img src=".github/frappe-framework-logo.png" height="150">
|
||||
<h1>
|
||||
<a href="https://frappe.io">
|
||||
<a href="https://frappeframework.com">
|
||||
frappe
|
||||
</a>
|
||||
</h1>
|
||||
|
|
@ -33,8 +33,8 @@
|
|||
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
|
||||
|
||||
### Table of Contents
|
||||
* [Installation](#installation)
|
||||
* [Documentation](https://frappe.io/docs)
|
||||
* [Installation](https://frappeframework.com/docs/user/en/installation)
|
||||
* [Documentation](https://frappeframework.com/docs)
|
||||
* [License](#license)
|
||||
|
||||
### Installation
|
||||
|
|
@ -49,7 +49,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
|
|||
### Website
|
||||
|
||||
For details and documentation, see the website
|
||||
[https://frappe.io](https://frappe.io)
|
||||
[https://frappeframework.com](https://frappeframework.com)
|
||||
|
||||
### License
|
||||
This repository has been released under the [MIT License](LICENSE).
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
"baseUrl": "http://test_site_ui:8000",
|
||||
"projectId": "92odwv",
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000
|
||||
}
|
||||
|
|
|
|||
45
cypress/integration/control_duration.js
Normal file
45
cypress/integration/control_duration.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
context('Control Duration', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
|
||||
function get_dialog_with_duration(hide_days=0, hide_seconds=0) {
|
||||
return cy.dialog({
|
||||
title: 'Duration',
|
||||
fields: [{
|
||||
'fieldname': 'duration',
|
||||
'fieldtype': 'Duration',
|
||||
'hide_days': hide_days,
|
||||
'hide_seconds': hide_seconds
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('should set duration', () => {
|
||||
get_dialog_with_duration().as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('.duration-input[data-duration=days]')
|
||||
.type(45, {force: true})
|
||||
.blur({force: true});
|
||||
cy.get('.duration-input[data-duration=minutes]')
|
||||
.type(30)
|
||||
.blur({force: true});
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().should('have.value', '45d 30m');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().blur();
|
||||
cy.get('.duration-picker').should('not.be.visible');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('duration');
|
||||
expect(value).to.equal(3889800);
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide days or seconds according to duration options', () => {
|
||||
get_dialog_with_duration(1, 1).as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().click();
|
||||
cy.get('.duration-input[data-duration=days]').should('not.be.visible');
|
||||
cy.get('.duration-input[data-duration=seconds]').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
context('Control Link', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for link'
|
||||
|
|
@ -30,7 +34,7 @@ context('Control Link', () => {
|
|||
|
||||
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
|
||||
cy.wait('@search_link');
|
||||
cy.get('@input').type('todo for link');
|
||||
cy.get('@input').type('todo for link', { delay: 200 });
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ context('Form', () => {
|
|||
it('create a new form', () => {
|
||||
cy.visit('/desk#Form/ToDo/New ToDo 1');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
|
||||
cy.wait(300);
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.server();
|
||||
cy.route({
|
||||
|
|
|
|||
|
|
@ -40,12 +40,12 @@ context('Grid Pagination', () => {
|
|||
cy.get('@table').find('.current-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
});
|
||||
it('deletes all rows', ()=> {
|
||||
cy.visit('/desk#Form/Contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
|
||||
cy.get('@table').find('button.grid-remove-all-rows').click();
|
||||
cy.get('.modal-dialog .btn-primary').contains('Yes').click();
|
||||
cy.get('@table').find('.grid-body .grid-row').should('have.length', 0);
|
||||
});
|
||||
// it('deletes all rows', ()=> {
|
||||
// cy.visit('/desk#Form/Contact/Test Contact');
|
||||
// cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
// cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
|
||||
// cy.get('@table').find('button.grid-remove-all-rows').click();
|
||||
// cy.get('.modal-dialog .btn-primary').contains('Yes').click();
|
||||
// cy.get('@table').find('.grid-body .grid-row').should('have.length', 0);
|
||||
// });
|
||||
});
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
context('Relative Timeframe', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
|
@ -10,14 +9,14 @@ context('Relative Timeframe', () => {
|
|||
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
});
|
||||
});
|
||||
it('set relative filter for Previous and check list', () => {
|
||||
it('sets relative timespan filter for last week and filters list', () => {
|
||||
cy.visit('/desk#List/ToDo/List');
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.get('.fieldname-select-area').should('exist');
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Previous");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("1 week");
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-box .btn:contains("Apply")').click();
|
||||
|
|
@ -29,13 +28,13 @@ context('Relative Timeframe', () => {
|
|||
cy.get('.remove-filter.btn').click();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
it('set relative filter for Next and check list', () => {
|
||||
it('sets relative timespan filter for next week and filters list', () => {
|
||||
cy.visit('/desk#List/ToDo/List');
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Next");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("1 week");
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-box .btn:contains("Apply")').click();
|
||||
|
|
@ -231,9 +231,8 @@ def get_site_config(sites_path=None, site_path=None):
|
|||
if os.path.exists(site_config):
|
||||
config.update(get_file_json(site_config))
|
||||
elif local.site and not local.flags.new_site:
|
||||
print("{0} does not exist".format(local.site))
|
||||
print("Site {0} does not exist".format(local.site))
|
||||
sys.exit(1)
|
||||
#raise IncorrectSitePath, "{0} does not exist".format(site_config)
|
||||
|
||||
return _dict(config)
|
||||
|
||||
|
|
@ -1559,10 +1558,10 @@ def get_doctype_app(doctype):
|
|||
|
||||
loggers = {}
|
||||
log_level = None
|
||||
def logger(module=None, with_more_info=True):
|
||||
def logger(module=None, with_more_info=False):
|
||||
'''Returns a python logger that uses StreamHandler'''
|
||||
from frappe.utils.logger import get_logger
|
||||
return get_logger(module or 'default', with_more_info=with_more_info)
|
||||
return get_logger(module=module, with_more_info=with_more_info)
|
||||
|
||||
def log_error(message=None, title=_("Error")):
|
||||
'''Log error to Error Log'''
|
||||
|
|
|
|||
|
|
@ -99,6 +99,16 @@ def application(request):
|
|||
frappe.monitor.stop(response)
|
||||
frappe.recorder.dump()
|
||||
|
||||
frappe.logger("web").info({
|
||||
"site": get_site_name(request.host),
|
||||
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
|
||||
"base_url": getattr(request, "base_url", "NOTFOUND"),
|
||||
"full_path": getattr(request, "full_path", "NOTFOUND"),
|
||||
"method": getattr(request, "method", "NOTFOUND"),
|
||||
"scheme": getattr(request, "scheme", "NOTFOUND"),
|
||||
"http_status_code": getattr(response, "status_code", "NOTFOUND")
|
||||
})
|
||||
|
||||
if response and hasattr(frappe.local, 'rate_limiter'):
|
||||
response.headers.extend(frappe.local.rate_limiter.headers())
|
||||
|
||||
|
|
@ -195,7 +205,6 @@ def handle_exception(e):
|
|||
frappe.local.login_manager.clear_cookies()
|
||||
|
||||
if http_status_code >= 500:
|
||||
frappe.logger().error('Request Error', exc_info=True)
|
||||
make_error_snapshot(e)
|
||||
|
||||
if return_as_message:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from frappe.email.inbox import get_email_accounts
|
|||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
|
||||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
|
||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.social.doctype.post.post import frequently_visited_links
|
||||
|
||||
def get_bootinfo():
|
||||
|
|
@ -84,6 +85,7 @@ def get_bootinfo():
|
|||
bootinfo.points = get_energy_points(frappe.session.user)
|
||||
bootinfo.frequently_visited_links = frequently_visited_links()
|
||||
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
|
||||
bootinfo.additional_filters_config = get_additional_filters_from_hooks()
|
||||
|
||||
return bootinfo
|
||||
|
||||
|
|
@ -106,7 +108,8 @@ def load_desktop_data(bootinfo):
|
|||
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.dashboards = frappe.get_list("Dashboard")
|
||||
bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
def get_allowed_pages(cache=False):
|
||||
return get_user_pages_or_reports('Page', cache=cache)
|
||||
|
|
@ -295,3 +298,11 @@ def get_link_preview_doctypes():
|
|||
link_preview_doctypes.append(custom.doc_type)
|
||||
|
||||
return link_preview_doctypes
|
||||
|
||||
def get_additional_filters_from_hooks():
|
||||
filter_config = frappe._dict()
|
||||
filter_hooks = frappe.get_hooks('filters_config')
|
||||
for hook in filter_hooks:
|
||||
filter_config.update(frappe.get_attr(hook)())
|
||||
|
||||
return filter_config
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ def pass_context(f):
|
|||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
ret = f(frappe._dict(ctx.obj), *args, **kwargs)
|
||||
try:
|
||||
ret = f(frappe._dict(ctx.obj), *args, **kwargs)
|
||||
except frappe.exceptions.SiteNotSpecifiedError as e:
|
||||
click.secho(str(e), fg='yellow')
|
||||
sys.exit(1)
|
||||
|
||||
if profile:
|
||||
pr.disable()
|
||||
|
|
@ -39,13 +43,14 @@ def pass_context(f):
|
|||
|
||||
return click.pass_context(_func)
|
||||
|
||||
def get_site(context):
|
||||
def get_site(context, raise_err=True):
|
||||
try:
|
||||
site = context.sites[0]
|
||||
return site
|
||||
except (IndexError, TypeError):
|
||||
print('Please specify --site sitename')
|
||||
sys.exit(1)
|
||||
if raise_err:
|
||||
raise frappe.SiteNotSpecifiedError
|
||||
return None
|
||||
|
||||
def popen(command, *args, **kwargs):
|
||||
output = kwargs.get('output', True)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import sys
|
|||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
|
||||
def _is_scheduler_enabled():
|
||||
enable_scheduler = False
|
||||
|
|
@ -30,6 +31,8 @@ def trigger_scheduler_event(context, event):
|
|||
frappe.utils.scheduler.trigger(site, event, now=True)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('enable-scheduler')
|
||||
@pass_context
|
||||
|
|
@ -45,6 +48,8 @@ def enable_scheduler(context):
|
|||
print("Enabled for", site)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('disable-scheduler')
|
||||
@pass_context
|
||||
|
|
@ -60,7 +65,8 @@ def disable_scheduler(context):
|
|||
print("Disabled for", site)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('scheduler')
|
||||
|
|
@ -120,7 +126,7 @@ def doctor(context, site=None):
|
|||
"Get diagnostic info about background workers"
|
||||
from frappe.utils.doctor import doctor as _doctor
|
||||
if not site:
|
||||
site = get_site(context)
|
||||
site = get_site(context, raise_err=False)
|
||||
return _doctor(site=site)
|
||||
|
||||
@click.command('show-pending-jobs')
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.utils import get_site_path, touch_file
|
||||
|
||||
|
|
@ -43,14 +44,16 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
|
|||
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
|
||||
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port)
|
||||
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
|
||||
db_port=db_port, new_site=True)
|
||||
|
||||
if len(frappe.utils.get_sites()) == 1:
|
||||
use(site)
|
||||
|
||||
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
|
||||
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
|
||||
no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None, db_port=None):
|
||||
no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None,
|
||||
db_port=None, new_site=False):
|
||||
"""Install a new Frappe site"""
|
||||
|
||||
if not force and os.path.exists(site):
|
||||
|
|
@ -79,7 +82,6 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
make_site_dirs()
|
||||
|
||||
installing = touch_file(get_site_path('locks', 'installing.lock'))
|
||||
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
|
||||
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
|
||||
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
|
||||
|
|
@ -96,15 +98,6 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
|
||||
print("*** Scheduler is", scheduler_status, "***")
|
||||
|
||||
def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
|
||||
installing = get_site_path('locks', 'installing.lock')
|
||||
|
||||
if installing and os.path.exists(installing):
|
||||
if mariadb_root_password:
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True, no_backup=True)
|
||||
shutil.rmtree(site)
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
@click.command('restore')
|
||||
@click.argument('sql-file-path')
|
||||
|
|
@ -122,30 +115,47 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
|
||||
|
||||
if not os.path.exists(sql_file_path):
|
||||
sql_file_path = '../' + sql_file_path
|
||||
base_path = '..'
|
||||
sql_file_path = os.path.join(base_path, sql_file_path)
|
||||
if not os.path.exists(sql_file_path):
|
||||
print('Invalid path {0}'.format(sql_file_path[3:]))
|
||||
sys.exit(1)
|
||||
elif sql_file_path.startswith(os.sep):
|
||||
base_path = os.sep
|
||||
else:
|
||||
base_path = '.'
|
||||
|
||||
|
||||
if sql_file_path.endswith('sql.gz'):
|
||||
sql_file_path = extract_sql_gzip(os.path.abspath(sql_file_path))
|
||||
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
|
||||
else:
|
||||
decompressed_file_name = sql_file_path
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
_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=sql_file_path,
|
||||
force=context.force)
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True)
|
||||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
with_public_files = os.path.join(base_path, with_public_files)
|
||||
public = extract_tar_files(site, with_public_files, 'public')
|
||||
os.remove(public)
|
||||
|
||||
if with_private_files:
|
||||
with_private_files = os.path.join(base_path, with_private_files)
|
||||
private = extract_tar_files(site, with_private_files, 'private')
|
||||
os.remove(private)
|
||||
|
||||
# Removing temporarily created file
|
||||
if decompressed_file_name != sql_file_path:
|
||||
os.remove(decompressed_file_name)
|
||||
|
||||
success_message = "Site {0} has been restored{1}".format(site, " with files" if (with_public_files or with_private_files) else "")
|
||||
click.secho(success_message, fg="green")
|
||||
|
||||
@click.command('reinstall')
|
||||
@click.option('--admin-password', help='Administrator Password for reinstalled site')
|
||||
@click.option('--mariadb-root-username', help='Root username for MariaDB')
|
||||
|
|
@ -192,6 +202,8 @@ def install_app(context, apps):
|
|||
_install_app(app, verbose=context.verbose)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('list-apps')
|
||||
@pass_context
|
||||
|
|
@ -221,7 +233,8 @@ def add_system_manager(context, email, first_name, last_name, send_welcome_email
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('disable-user')
|
||||
@click.argument('email')
|
||||
|
|
@ -252,6 +265,8 @@ def migrate(context, rebuild_website=False, skip_failing=False):
|
|||
migrate(context.verbose, rebuild_website=rebuild_website, skip_failing=skip_failing)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
print("Compiling Python Files...")
|
||||
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
|
||||
|
|
@ -263,7 +278,12 @@ def migrate_to(context, frappe_provider):
|
|||
"Migrates site to the specified provider"
|
||||
from frappe.integrations.frappe_providers import migrate_to
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
migrate_to(site, frappe_provider)
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('run-patch')
|
||||
@click.argument('module')
|
||||
|
|
@ -278,6 +298,8 @@ def run_patch(context, module):
|
|||
frappe.modules.patch_handler.run_single(module, force=context.force)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('reload-doc')
|
||||
@click.argument('module')
|
||||
|
|
@ -294,6 +316,8 @@ def reload_doc(context, module, doctype, docname):
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('reload-doctype')
|
||||
@click.argument('doctype')
|
||||
|
|
@ -308,6 +332,8 @@ def reload_doctype(context, doctype):
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('add-to-hosts')
|
||||
@pass_context
|
||||
|
|
@ -315,6 +341,8 @@ def add_to_hosts(context):
|
|||
"Add site to hosts"
|
||||
for site in context.sites:
|
||||
frappe.commands.popen('echo 127.0.0.1\t{0} | sudo tee -a /etc/hosts'.format(site))
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('use')
|
||||
@click.argument('site')
|
||||
|
|
@ -328,7 +356,7 @@ def use(site, sites_path='.'):
|
|||
sitefile.write(site)
|
||||
print("Current Site set to {}".format(site))
|
||||
else:
|
||||
print("{} does not exist".format(site))
|
||||
print("Site {} does not exist".format(site))
|
||||
|
||||
@click.command('backup')
|
||||
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
|
||||
|
|
@ -361,6 +389,9 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
|
|||
print("Private files: ", odb.backup_path_private_files)
|
||||
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
@click.command('remove-from-installed-apps')
|
||||
|
|
@ -376,6 +407,8 @@ def remove_from_installed_apps(context, app):
|
|||
remove_from_installed_apps(app)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('uninstall-app')
|
||||
@click.argument('app')
|
||||
|
|
@ -392,6 +425,8 @@ def uninstall(context, app, dry_run=False, yes=False):
|
|||
remove_app(app, dry_run, yes)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('drop-site')
|
||||
|
|
@ -422,7 +457,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
|
|||
else:
|
||||
click.echo("="*80)
|
||||
click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
|
||||
click.echo("Reason: {reason}{sep}".format(reason=err[1], sep="\n"))
|
||||
click.echo("Reason: {reason}{sep}".format(reason=str(err), sep="\n"))
|
||||
click.echo("Fix the issue and try again.")
|
||||
click.echo(
|
||||
"Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
|
||||
|
|
@ -483,6 +518,8 @@ def set_admin_password(context, admin_password, logout_all_sessions=False):
|
|||
admin_password = None
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('set-last-active-for-user')
|
||||
@click.option('--user', help="Setup last active date for user")
|
||||
|
|
@ -528,6 +565,8 @@ def publish_realtime(context, event, message, room, user, doctype, docname, afte
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('browse')
|
||||
@click.argument('site', required=False)
|
||||
|
|
@ -555,6 +594,8 @@ def start_recording(context):
|
|||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.recorder.start()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('stop-recording')
|
||||
|
|
@ -563,6 +604,8 @@ def stop_recording(context):
|
|||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.recorder.stop()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
commands = [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import unicode_literals, absolute_import, print_function
|
||||
import click
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
|
||||
# translation
|
||||
@click.command('build-message-files')
|
||||
|
|
@ -15,6 +16,8 @@ def build_message_files(context):
|
|||
frappe.translate.rebuild_all_translation_files()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('new-language') #, help="Create lang-code.csv for given app")
|
||||
@pass_context
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import json, os, sys, subprocess
|
|||
from distutils.spawn import find_executable
|
||||
import frappe
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.utils import update_progress_bar, get_bench_path
|
||||
from frappe.utils.response import json_handler
|
||||
from coverage import Coverage
|
||||
|
|
@ -51,7 +52,8 @@ def clear_cache(context):
|
|||
frappe.website.render.clear_cache()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('clear-website-cache')
|
||||
@pass_context
|
||||
|
|
@ -65,7 +67,8 @@ def clear_website_cache(context):
|
|||
frappe.website.render.clear_cache()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('destroy-all-sessions')
|
||||
@click.option('--reason')
|
||||
|
|
@ -81,7 +84,8 @@ def destroy_all_sessions(context, reason=None):
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('show-config')
|
||||
@pass_context
|
||||
|
|
@ -117,7 +121,8 @@ def reset_perms(context):
|
|||
reset_perms(d)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('execute')
|
||||
@click.argument('method')
|
||||
|
|
@ -164,6 +169,9 @@ def execute(context, method, args=None, kwargs=None, profile=False):
|
|||
if ret:
|
||||
print(json.dumps(ret, default=json_handler))
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('add-to-email-queue')
|
||||
@click.argument('email-path')
|
||||
|
|
@ -197,7 +205,8 @@ def export_doc(context, doctype, docname):
|
|||
frappe.modules.export_doc(doctype, docname)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('export-json')
|
||||
@click.argument('doctype')
|
||||
|
|
@ -214,7 +223,8 @@ def export_json(context, doctype, path, name=None):
|
|||
data_import.export_json(doctype, path, name=name)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('export-csv')
|
||||
@click.argument('doctype')
|
||||
|
|
@ -230,7 +240,8 @@ def export_csv(context, doctype, path):
|
|||
data_import.export_csv(doctype, path)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('export-fixtures')
|
||||
@click.option('--app', default=None, help='Export fixtures of a specific app')
|
||||
|
|
@ -245,7 +256,8 @@ def export_fixtures(context, app=None):
|
|||
export_fixtures(app=app)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('import-doc')
|
||||
@click.argument('path')
|
||||
|
|
@ -267,7 +279,8 @@ def import_doc(context, path, force=False):
|
|||
data_import.import_doc(path, overwrite=context.force)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('import-csv')
|
||||
@click.argument('path')
|
||||
|
|
@ -364,6 +377,8 @@ def mariadb(context):
|
|||
import os
|
||||
|
||||
site = get_site(context)
|
||||
if not site:
|
||||
raise SiteNotSpecifiedError
|
||||
frappe.init(site=site)
|
||||
|
||||
# This is assuming you're within the bench instance.
|
||||
|
|
@ -487,7 +502,17 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
|
|||
if coverage:
|
||||
# Generate coverage report only for app that is being tested
|
||||
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
|
||||
cov = Coverage(source=[source_path], omit=['*.html', '*.js', '*.xml', '*.css', '*/doctype/*/*_dashboard.py', '*/patches/*'])
|
||||
cov = Coverage(source=[source_path], omit=[
|
||||
'*.html',
|
||||
'*.js',
|
||||
'*.xml',
|
||||
'*.css',
|
||||
'*.less',
|
||||
'*.scss',
|
||||
'*.vue',
|
||||
'*/doctype/*/*_dashboard.py',
|
||||
'*/patches/*'
|
||||
])
|
||||
cov.start()
|
||||
|
||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
|
||||
|
|
@ -577,7 +602,8 @@ def request(context, args=None, path=None):
|
|||
print(frappe.response)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('make-app')
|
||||
@click.argument('destination')
|
||||
|
|
@ -658,7 +684,8 @@ def rebuild_global_search(context, static_pages=False):
|
|||
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('auto-deploy')
|
||||
@click.argument('app')
|
||||
|
|
|
|||
|
|
@ -158,11 +158,7 @@ class TestAccessLog(unittest.TestCase):
|
|||
request = requests.post(private_file_link, headers=self.header)
|
||||
last_doc = frappe.get_last_doc('Access Log')
|
||||
|
||||
if request.status_code == 403:
|
||||
# if file is not accessible, access log wont be generated
|
||||
pass
|
||||
|
||||
else:
|
||||
if request.ok:
|
||||
# check for the access log of downloaded file
|
||||
self.assertEqual(new_private_file.doctype, last_doc.export_from)
|
||||
self.assertEqual(new_private_file.name, last_doc.reference_document)
|
||||
|
|
|
|||
|
|
@ -2,20 +2,21 @@
|
|||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
from collections import Counter
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address, get_fullname, strip_html, cstr
|
||||
from frappe.core.doctype.communication.email import (validate_email,
|
||||
notify, _notify, update_parent_mins_to_first_response)
|
||||
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds
|
||||
from frappe.core.doctype.communication.email import validate_email, notify, _notify
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils.bot import BotReply
|
||||
from frappe.utils import parse_addr
|
||||
from frappe.core.doctype.comment.comment import update_comment_in_doc
|
||||
from email.utils import parseaddr
|
||||
from six.moves.urllib.parse import unquote
|
||||
from collections import Counter
|
||||
from frappe.utils.user import is_system_user
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
|
|
@ -119,7 +120,7 @@ class Communication(Document):
|
|||
update_comment_in_doc(self)
|
||||
|
||||
if self.comment_type != 'Updated':
|
||||
update_parent_mins_to_first_response(self)
|
||||
update_parent_document_on_communication(self)
|
||||
self.bot_reply()
|
||||
|
||||
def on_trash(self):
|
||||
|
|
@ -258,7 +259,12 @@ class Communication(Document):
|
|||
|
||||
# Timeline Links
|
||||
def set_timeline_links(self):
|
||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
|
||||
contacts = []
|
||||
if (self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")) or \
|
||||
frappe.flags.in_test:
|
||||
|
||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
|
||||
|
||||
for contact_name in contacts:
|
||||
self.add_link('Contact', contact_name)
|
||||
|
||||
|
|
@ -423,3 +429,63 @@ def get_email_without_link(email):
|
|||
email_host = email.split("@")[1]
|
||||
|
||||
return "{0}@{1}".format(email_id, email_host)
|
||||
|
||||
def update_parent_document_on_communication(doc):
|
||||
"""Update mins_to_first_communication of parent document based on who is replying."""
|
||||
|
||||
parent = get_parent_doc(doc)
|
||||
if not parent:
|
||||
return
|
||||
|
||||
# update parent mins_to_first_communication only if we create the Email communication
|
||||
# ignore in case of only Comment is added
|
||||
if doc.communication_type == "Comment":
|
||||
return
|
||||
|
||||
status_field = parent.meta.get_field("status")
|
||||
if status_field:
|
||||
options = (status_field.options or "").splitlines()
|
||||
|
||||
# if status has a "Replied" option, then update the status for received communication
|
||||
if ("Replied" in options) and doc.sent_or_received == "Received":
|
||||
parent.db_set("status", "Open")
|
||||
parent.run_method("handle_hold_time", "Replied")
|
||||
apply_assignment_rule(parent)
|
||||
else:
|
||||
# update the modified date for document
|
||||
parent.update_modified()
|
||||
|
||||
update_mins_to_first_communication(parent, doc)
|
||||
set_avg_response_time(parent, doc)
|
||||
parent.run_method("notify_communication", doc)
|
||||
parent.notify_update()
|
||||
|
||||
def update_mins_to_first_communication(parent, communication):
|
||||
if parent.meta.has_field("mins_to_first_response") and not parent.get("mins_to_first_response"):
|
||||
if is_system_user(communication.sender):
|
||||
first_responded_on = communication.creation
|
||||
if parent.meta.has_field("first_responded_on") and communication.sent_or_received == "Sent":
|
||||
parent.db_set("first_responded_on", first_responded_on)
|
||||
parent.db_set("mins_to_first_response", round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2)
|
||||
|
||||
def set_avg_response_time(parent, communication):
|
||||
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":
|
||||
# avg response time for all the responses
|
||||
communications = frappe.get_list("Communication", filters={
|
||||
"reference_doctype": parent.doctype,
|
||||
"reference_name": parent.name
|
||||
},
|
||||
fields=["sent_or_received", "name", "creation"],
|
||||
order_by="creation"
|
||||
)
|
||||
|
||||
if len(communications):
|
||||
response_times = []
|
||||
for i in range(len(communications)):
|
||||
if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received":
|
||||
response_time = round(time_diff_in_seconds(communications[i].creation, communications[i-1].creation), 2)
|
||||
if response_time > 0:
|
||||
response_times.append(response_time)
|
||||
if response_times:
|
||||
avg_response_time = sum(response_times) / len(response_times)
|
||||
parent.db_set("avg_response_time", avg_response_time)
|
||||
|
|
@ -9,7 +9,7 @@ import json
|
|||
from email.utils import formataddr
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils import (get_url, get_formatted_email, cint,
|
||||
validate_email_address, split_emails, time_diff_in_seconds, parse_addr, get_datetime)
|
||||
validate_email_address, split_emails, parse_addr, get_datetime)
|
||||
from frappe.email.email_body import get_message_id
|
||||
import frappe.email.smtp
|
||||
import time
|
||||
|
|
@ -172,33 +172,6 @@ def _notify(doc, print_html=None, print_format=None, attachments=None,
|
|||
print_letterhead=frappe.flags.print_letterhead
|
||||
)
|
||||
|
||||
def update_parent_mins_to_first_response(doc):
|
||||
"""Update mins_to_first_communication of parent document based on who is replying."""
|
||||
|
||||
parent = get_parent_doc(doc)
|
||||
if not parent:
|
||||
return
|
||||
|
||||
# update parent mins_to_first_communication only if we create the Email communication
|
||||
# ignore in case of only Comment is added
|
||||
if doc.communication_type == "Comment":
|
||||
return
|
||||
|
||||
status_field = parent.meta.get_field("status")
|
||||
if status_field:
|
||||
options = (status_field.options or '').splitlines()
|
||||
|
||||
# if status has a "Replied" option, then update the status for received communication
|
||||
if ('Replied' in options) and doc.sent_or_received=="Received":
|
||||
parent.db_set("status", "Open")
|
||||
else:
|
||||
# update the modified date for document
|
||||
parent.update_modified()
|
||||
|
||||
update_mins_to_first_communication(parent, doc)
|
||||
parent.run_method('notify_communication', doc)
|
||||
parent.notify_update()
|
||||
|
||||
def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_account=False):
|
||||
doc.all_email_addresses = []
|
||||
doc.sent_email_addresses = []
|
||||
|
|
@ -499,15 +472,6 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments
|
|||
traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail")
|
||||
raise
|
||||
|
||||
def update_mins_to_first_communication(parent, communication):
|
||||
if parent.meta.has_field('mins_to_first_response') and not parent.get('mins_to_first_response'):
|
||||
if frappe.db.get_all('User', filters={'email': communication.sender,
|
||||
'user_type': 'System User', 'enabled': 1}, limit=1):
|
||||
first_responded_on = communication.creation
|
||||
if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent":
|
||||
parent.db_set('first_responded_on', first_responded_on)
|
||||
parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def mark_email_as_seen(name=None):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -202,6 +202,8 @@ class TestCommunication(unittest.TestCase):
|
|||
self.assertIn(("Note", note.name), doc_links)
|
||||
|
||||
def create_email_account():
|
||||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
|
||||
|
||||
frappe.flags.mute_emails = False
|
||||
frappe.flags.sent_mail = None
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
"fieldname",
|
||||
"precision",
|
||||
"length",
|
||||
"hide_days",
|
||||
"hide_seconds",
|
||||
"reqd",
|
||||
"search_index",
|
||||
"in_list_view",
|
||||
|
|
@ -87,7 +89,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -450,6 +452,20 @@
|
|||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Days"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Seconds"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
|
|
@ -461,7 +477,7 @@
|
|||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-27 11:38:21.223185",
|
||||
"modified": "2020-02-06 09:06:25.224413",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -688,6 +688,9 @@ def validate_fields(meta):
|
|||
|
||||
def check_link_table_options(docname, d):
|
||||
if frappe.flags.in_patch: return
|
||||
|
||||
if frappe.flags.in_fixtures: return
|
||||
|
||||
if d.fieldtype in ("Link",) + table_fields:
|
||||
if not d.options:
|
||||
frappe.throw(_("{0}: Options required for Link or Table type field {1} in row {2}").format(docname, d.label, d.idx), DoctypeLinkError)
|
||||
|
|
@ -908,6 +911,8 @@ def validate_fields(meta):
|
|||
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
|
||||
|
||||
def check_child_table_option(docfield):
|
||||
|
||||
if frappe.flags.in_fixtures: return
|
||||
if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return
|
||||
|
||||
doctype = docfield.options
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ class File(Document):
|
|||
def before_insert(self):
|
||||
frappe.local.rollback_observers.append(self)
|
||||
self.set_folder_name()
|
||||
if self.file_name:
|
||||
self.file_name = re.sub(r'/', '', self.file_name)
|
||||
self.content = self.get("content", None)
|
||||
self.decode = self.get("decode", False)
|
||||
if self.content:
|
||||
|
|
@ -180,11 +182,11 @@ 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 DuplicateEntryError
|
||||
# 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.DuplicateEntryError)
|
||||
frappe.FileAlreadyAttachedException)
|
||||
# else just use the url, to avoid uploading a duplicate
|
||||
else:
|
||||
self.file_url = duplicate_file.file_url
|
||||
|
|
@ -192,6 +194,8 @@ class File(Document):
|
|||
def set_file_name(self):
|
||||
if not self.file_name and self.file_url:
|
||||
self.file_name = self.file_url.split('/')[-1]
|
||||
else:
|
||||
self.file_name = re.sub(r'/', '', self.file_name)
|
||||
|
||||
def generate_content_hash(self):
|
||||
if self.content_hash or not self.file_url or self.file_url.startswith('http'):
|
||||
|
|
@ -405,6 +409,12 @@ class File(Document):
|
|||
frappe.throw(_("URL must start with 'http://' or 'https://'"))
|
||||
return
|
||||
|
||||
if not self.file_url.startswith(("http://", "https://")):
|
||||
# local file
|
||||
root_files_path = get_files_path(is_private=self.is_private)
|
||||
if not os.path.commonpath([root_files_path]) == os.path.commonpath([root_files_path, self.get_full_path()]):
|
||||
# basically the file url is skewed to not point to /files/ or /private/files
|
||||
frappe.throw(_("{0} is not a valid file url").format(self.file_url))
|
||||
self.file_url = unquote(self.file_url)
|
||||
self.file_size = frappe.form_dict.file_size or self.file_size
|
||||
|
||||
|
|
@ -704,7 +714,12 @@ def remove_all(dt, dn, from_delete=False):
|
|||
try:
|
||||
for fid in frappe.db.sql_list("""select name from `tabFile` where
|
||||
attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
|
||||
remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn, from_delete=from_delete)
|
||||
if from_delete:
|
||||
# If deleting a doc, directly delete files
|
||||
frappe.delete_doc("File", fid, ignore_permissions=True)
|
||||
else:
|
||||
# Removes file and adds a comment in the document it is attached to
|
||||
remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn, from_delete=from_delete)
|
||||
except Exception as e:
|
||||
if e.args[0]!=1054: raise # (temp till for patched)
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class ScheduledJobType(Document):
|
|||
|
||||
def log_status(self, status):
|
||||
# log file
|
||||
frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
|
||||
frappe.logger("scheduler").info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
|
||||
self.update_scheduler_log(status)
|
||||
|
||||
def update_scheduler_log(self, status):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import unicode_literals, print_function
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
|
||||
from frappe.utils import cint, flt, has_gravatar, 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
|
||||
|
|
@ -841,11 +841,11 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
|
||||
def get_total_users():
|
||||
"""Returns total no. of system users"""
|
||||
return frappe.db.sql('''SELECT SUM(`simultaneous_sessions`)
|
||||
return flt(frappe.db.sql('''SELECT SUM(`simultaneous_sessions`)
|
||||
FROM `tabUser`
|
||||
WHERE `enabled` = 1
|
||||
AND `user_type` = 'System User'
|
||||
AND `name` NOT IN ({})'''.format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0]
|
||||
AND `name` NOT IN ({})'''.format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0])
|
||||
|
||||
def get_system_users(exclude_users=None, limit=None):
|
||||
if not exclude_users:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"column_break_6",
|
||||
"fieldtype",
|
||||
"precision",
|
||||
"hide_seconds",
|
||||
"hide_days",
|
||||
"options",
|
||||
"fetch_from",
|
||||
"fetch_if_empty",
|
||||
|
|
@ -56,368 +58,382 @@
|
|||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Document",
|
||||
"oldfieldname": "dt",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"bold": 1,
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Document",
|
||||
"oldfieldname": "dt",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"label": "Label",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data"
|
||||
"bold": 1,
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"label": "Label",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "label_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Label Help",
|
||||
"oldfieldtype": "HTML"
|
||||
"fieldname": "label_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Label Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Select the label after which you want to insert new field.",
|
||||
"fieldname": "insert_after",
|
||||
"fieldtype": "Select",
|
||||
"label": "Insert After",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "insert_after",
|
||||
"oldfieldtype": "Select"
|
||||
"description": "Select the label after which you want to insert new field.",
|
||||
"fieldname": "insert_after",
|
||||
"fieldtype": "Select",
|
||||
"label": "Insert After",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "insert_after",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"reqd": 1
|
||||
"bold": 1,
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
},
|
||||
{
|
||||
"fieldname": "options_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Options Help",
|
||||
"oldfieldtype": "HTML"
|
||||
"fieldname": "options_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Options Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On"
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On"
|
||||
},
|
||||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"length": 255
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Field Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Field Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Permission Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Permission Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data"
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_width": "50%",
|
||||
"width": "50%"
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_width": "50%",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory Field",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory Field",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Link\"",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Link\"",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden"
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Print Width",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Print Width",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "No Copy",
|
||||
"oldfieldname": "no_copy",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "No Copy",
|
||||
"oldfieldname": "no_copy",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "search_index",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Index",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
"default": "0",
|
||||
"fieldname": "search_index",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Index",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Seconds"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Days"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-27 11:40:48.325481",
|
||||
"modified": "2020-02-06 23:43:00.123575",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "dt,label,fieldtype,options",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ class CustomField(Document):
|
|||
if not self.fieldname:
|
||||
frappe.throw(_("Fieldname not set for Custom Field"))
|
||||
|
||||
if self.fieldname in fieldnames:
|
||||
frappe.throw(_("A field with the name '{}' already exists in doctype {}.").format(self.fieldname, self.dt))
|
||||
|
||||
if self.get('translatable', 0) and not supports_translation(self.fieldtype):
|
||||
self.translatable = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ docfield_properties = {
|
|||
'allow_bulk_edit': 'Check',
|
||||
'auto_repeat': 'Link',
|
||||
'allow_in_quick_entry': 'Check',
|
||||
'hide_border': 'Check'
|
||||
'hide_border': 'Check',
|
||||
'hide_days': 'Check',
|
||||
'hide_seconds': 'Check'
|
||||
}
|
||||
|
||||
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
"label",
|
||||
"fieldtype",
|
||||
"fieldname",
|
||||
"hide_seconds",
|
||||
"hide_days",
|
||||
"reqd",
|
||||
"unique",
|
||||
"in_list_view",
|
||||
|
|
@ -58,350 +60,364 @@
|
|||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "label_and_type",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Label and Type"
|
||||
"fieldname": "label_and_type",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Label and Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data",
|
||||
"search_index": 1
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mandatory",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mandatory",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
},
|
||||
{
|
||||
"fieldname": "permissions",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Permissions"
|
||||
"fieldname": "permissions",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Permissions"
|
||||
},
|
||||
{
|
||||
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18",
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"oldfieldname": "depends_on",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "JS"
|
||||
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18",
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"oldfieldname": "depends_on",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Perm Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Perm Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden",
|
||||
"oldfieldname": "hidden",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden",
|
||||
"oldfieldname": "hidden",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.fieldtype == \"Table\"",
|
||||
"fieldname": "allow_bulk_edit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Bulk Edit"
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.fieldtype == \"Table\"",
|
||||
"fieldname": "allow_bulk_edit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Bulk Edit"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On",
|
||||
"options": "JS"
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
"default": "0",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.fieldtype == 'Link')",
|
||||
"fieldname": "remember_last_selected_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Remember Last Selected Value"
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.fieldtype == 'Link')",
|
||||
"fieldname": "remember_last_selected_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Remember Last Selected Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "display",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Display"
|
||||
"fieldname": "display",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Display"
|
||||
},
|
||||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Filter",
|
||||
"oldfieldname": "in_filter",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"default": "0",
|
||||
"fieldname": "in_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Filter",
|
||||
"oldfieldname": "in_filter",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
},
|
||||
{
|
||||
"description": "Print Width of the field, if the field is a column in a table",
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Print Width",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"description": "Print Width of the field, if the field is a column in a table",
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Print Width",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:cur_frm.doc.istable",
|
||||
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
"depends_on": "eval:cur_frm.doc.istable",
|
||||
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_custom_field",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Custom Field",
|
||||
"read_only": 1
|
||||
"default": "0",
|
||||
"fieldname": "is_custom_field",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Custom Field",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "property_depends_on_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Property Depends On"
|
||||
"fieldname": "property_depends_on_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Property Depends On"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"options": "JS"
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_33",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_33",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"options": "JS"
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Seconds"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Days"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-27 11:39:26.389300",
|
||||
"modified": "2020-06-02 23:45:46.810868",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ class MariaDBDatabase(Database):
|
|||
'Signature': ('longtext', ''),
|
||||
'Color': ('varchar', self.VARCHAR_LEN),
|
||||
'Barcode': ('longtext', ''),
|
||||
'Geolocation': ('longtext', '')
|
||||
'Geolocation': ('longtext', ''),
|
||||
'Duration': ('decimal', '18,6')
|
||||
}
|
||||
|
||||
def get_connection(self):
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ CREATE TABLE `tabDocField` (
|
|||
`length` int(11) NOT NULL DEFAULT 0,
|
||||
`translatable` int(1) NOT NULL DEFAULT 0,
|
||||
`hide_border` int(1) NOT NULL DEFAULT 0,
|
||||
`hide_days` int(1) NOT NULL DEFAULT 0,
|
||||
`hide_seconds` int(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`),
|
||||
KEY `label` (`label`),
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ class PostgresDatabase(Database):
|
|||
'Signature': ('text', ''),
|
||||
'Color': ('varchar', self.VARCHAR_LEN),
|
||||
'Barcode': ('text', ''),
|
||||
'Geolocation': ('text', '')
|
||||
'Geolocation': ('text', ''),
|
||||
'Duration': ('decimal', '18,6')
|
||||
}
|
||||
|
||||
def get_connection(self):
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ CREATE TABLE "tabDocField" (
|
|||
"length" bigint NOT NULL DEFAULT 0,
|
||||
"translatable" smallint NOT NULL DEFAULT 0,
|
||||
"hide_border" smallint NOT NULL DEFAULT 0,
|
||||
"hide_days" smallint NOT NULL DEFAULT 0,
|
||||
"hide_seconds" smallint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("name")
|
||||
) ;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,26 @@ from json import loads, dumps
|
|||
from frappe import _, DoesNotExistError, ValidationError, _dict
|
||||
from frappe.boot import get_allowed_pages, get_allowed_reports
|
||||
from six import string_types
|
||||
from functools import wraps
|
||||
from frappe.cache_manager import (
|
||||
build_domain_restriced_doctype_cache,
|
||||
build_domain_restriced_page_cache,
|
||||
build_table_count_cache
|
||||
)
|
||||
|
||||
def handle_not_exist(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except DoesNotExistError:
|
||||
if frappe.message_log:
|
||||
frappe.message_log.pop()
|
||||
return []
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Workspace:
|
||||
def __init__(self, page_name):
|
||||
self.page_name = page_name
|
||||
|
|
@ -157,7 +171,7 @@ class Workspace:
|
|||
'user_can_dismiss': self.onboarding_doc.user_can_dismiss,
|
||||
'items': self.get_onboarding_steps()
|
||||
}
|
||||
|
||||
@handle_not_exist
|
||||
def get_cards(self):
|
||||
cards = self.doc.cards
|
||||
if not self.doc.hide_custom:
|
||||
|
|
@ -169,8 +183,8 @@ class Workspace:
|
|||
|
||||
def _doctype_contains_a_record(name):
|
||||
exists = self.table_counts.get(name, None)
|
||||
if exists is None:
|
||||
if not frappe.db.get_value('DocType', name, 'issingle', cache=True):
|
||||
if not exists:
|
||||
if not frappe.db.get_value('DocType', name, 'issingle'):
|
||||
exists = frappe.db.count(name)
|
||||
else:
|
||||
exists = True
|
||||
|
|
@ -227,6 +241,7 @@ class Workspace:
|
|||
|
||||
return new_data
|
||||
|
||||
@handle_not_exist
|
||||
def get_charts(self):
|
||||
all_charts = []
|
||||
if frappe.has_permission("Dashboard Chart", throw=False):
|
||||
|
|
@ -242,6 +257,7 @@ class Workspace:
|
|||
|
||||
return all_charts
|
||||
|
||||
@handle_not_exist
|
||||
def get_shortcuts(self):
|
||||
|
||||
def _in_active_domains(item):
|
||||
|
|
@ -272,6 +288,7 @@ class Workspace:
|
|||
|
||||
return items
|
||||
|
||||
@handle_not_exist
|
||||
def get_onboarding_steps(self):
|
||||
steps = []
|
||||
for doc in self.onboarding_doc.get_steps():
|
||||
|
|
@ -296,21 +313,15 @@ def get_desktop_page(page):
|
|||
Returns:
|
||||
dict: dictionary of cards, charts and shortcuts to be displayed on website
|
||||
"""
|
||||
try:
|
||||
wspace = Workspace(page)
|
||||
wspace.build_workspace()
|
||||
return {
|
||||
'charts': wspace.charts,
|
||||
'shortcuts': wspace.shortcuts,
|
||||
'cards': wspace.cards,
|
||||
'onboarding': wspace.onboarding,
|
||||
'allow_customization': not wspace.doc.disable_user_customization
|
||||
}
|
||||
|
||||
except DoesNotExistError:
|
||||
if frappe.message_log:
|
||||
frappe.message_log.pop()
|
||||
return None
|
||||
wspace = Workspace(page)
|
||||
wspace.build_workspace()
|
||||
return {
|
||||
'charts': wspace.charts,
|
||||
'shortcuts': wspace.shortcuts,
|
||||
'cards': wspace.cards,
|
||||
'onboarding': wspace.onboarding,
|
||||
'allow_customization': not wspace.doc.disable_user_customization
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_desk_sidebar_items(flatten=False):
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
render_filters_table: function(frm) {
|
||||
frm.set_df_property("filters_section", "hidden", 0);
|
||||
let is_document_type = frm.doc.chart_type!== 'Report' && frm.doc.chart_type!=='Custom';
|
||||
let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default;
|
||||
|
||||
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
|
||||
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
|
|
@ -268,6 +269,18 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
let filters = JSON.parse(frm.doc.filters_json || '[]');
|
||||
var filters_set = false;
|
||||
|
||||
// Set dynamic filters for reports
|
||||
if (frm.doc.chart_type == 'Report') {
|
||||
let set_filters = false;
|
||||
frm.chart_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 = [
|
||||
|
|
@ -292,6 +305,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
}
|
||||
} else if (frm.chart_filters.length) {
|
||||
fields = frm.chart_filters.filter(f => f.fieldname);
|
||||
|
||||
fields.map( f => {
|
||||
if (filters[f.fieldname]) {
|
||||
let condition = '=';
|
||||
|
|
@ -318,7 +332,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Set Filters'),
|
||||
fields: fields,
|
||||
fields: fields.filter(f => !is_dynamic_filter(f)),
|
||||
primary_action: function() {
|
||||
let values = this.get_values();
|
||||
if (values) {
|
||||
|
|
@ -351,8 +365,15 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
}
|
||||
|
||||
dialog.show();
|
||||
//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});
|
||||
|
||||
if (frm.doc.chart_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);
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
|
|||
to_date = datetime.datetime.now()
|
||||
|
||||
doctype = chart.document_type
|
||||
unit_function = get_unit_function(doctype, chart.based_on, timegrain)
|
||||
datefield = chart.based_on
|
||||
aggregate_function = get_aggregate_function(chart.chart_type)
|
||||
value_field = chart.value_based_on or '1'
|
||||
|
|
@ -150,23 +149,18 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
|
|||
data = frappe.db.get_list(
|
||||
doctype,
|
||||
fields = [
|
||||
'extract(year from `tab{doctype}`.{datefield}) as _year'.format(doctype=doctype, datefield=datefield),
|
||||
'{} as _unit'.format(unit_function),
|
||||
'{} as _unit'.format(datefield),
|
||||
'{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
|
||||
],
|
||||
filters = filters,
|
||||
group_by = '_year, _unit',
|
||||
order_by = '_year asc, _unit asc',
|
||||
group_by = '_unit',
|
||||
order_by = '_unit asc',
|
||||
as_list = True,
|
||||
ignore_ifnull = True
|
||||
)
|
||||
|
||||
result = get_result(data, timegrain, from_date, to_date)
|
||||
|
||||
# result given as year, unit -> convert it to end of period of that unit
|
||||
result = convert_to_dates(data, timegrain)
|
||||
|
||||
# add missing data points for periods where there was no result
|
||||
result = add_missing_values(result, timegrain, timespan, from_date, to_date)
|
||||
chart_config = {
|
||||
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
|
||||
"datasets": [{
|
||||
|
|
@ -261,75 +255,22 @@ def get_aggregate_function(chart_type):
|
|||
}[chart_type]
|
||||
|
||||
|
||||
def convert_to_dates(data, timegrain):
|
||||
""" Converts individual dates within data to the end of period """
|
||||
result = []
|
||||
for d in data:
|
||||
if d[2] != 0:
|
||||
if timegrain == 'Daily':
|
||||
result.append([add_to_date('{:d}-01-01'.format(int(d[0])), days = d[1] - 1), d[2]])
|
||||
elif timegrain == 'Weekly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]])
|
||||
elif timegrain == 'Monthly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]])
|
||||
elif timegrain == 'Quarterly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]])
|
||||
elif timegrain == 'Yearly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]])
|
||||
result[-1][0] = getdate(result[-1][0])
|
||||
|
||||
return result
|
||||
|
||||
def get_unit_function(doctype, datefield, timegrain):
|
||||
unit_function = ''
|
||||
if timegrain=='Daily':
|
||||
if frappe.db.db_type == 'mariadb':
|
||||
unit_function = 'dayofyear(`tab{doctype}`.{datefield})'.format(
|
||||
doctype=doctype, datefield=datefield)
|
||||
else:
|
||||
unit_function = 'extract(doy from `tab{doctype}`.{datefield})'.format(
|
||||
doctype=doctype, datefield=datefield)
|
||||
|
||||
else:
|
||||
unit_function = 'extract({unit} from `tab{doctype}`.{datefield})'.format(
|
||||
unit = timegrain[:-2].lower(), doctype=doctype, datefield=datefield)
|
||||
|
||||
return unit_function
|
||||
|
||||
def add_missing_values(data, timegrain, timespan, from_date, to_date):
|
||||
# add missing intervals
|
||||
def get_result(data, timegrain, from_date, to_date):
|
||||
start_date = getdate(from_date)
|
||||
end_date = getdate(to_date)
|
||||
result = []
|
||||
|
||||
if timespan != 'All Time':
|
||||
first_expected_date = get_period_ending(from_date, timegrain)
|
||||
# fill out data before the first data point
|
||||
first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1))
|
||||
while first_data_point_date > first_expected_date:
|
||||
result.append([first_expected_date, 0.0])
|
||||
first_expected_date = get_next_expected_date(first_expected_date, timegrain)
|
||||
while start_date <= end_date:
|
||||
next_date = get_next_expected_date(start_date, timegrain)
|
||||
result.append([next_date, 0.0])
|
||||
start_date = next_date
|
||||
|
||||
# fill data points and missing points
|
||||
for i, d in enumerate(data):
|
||||
result.append(d)
|
||||
|
||||
next_expected_date = get_next_expected_date(d[0], timegrain)
|
||||
|
||||
if i < len(data)-1:
|
||||
next_date = data[i+1][0]
|
||||
else:
|
||||
# already reached at end of data, see if we need any more dates
|
||||
next_date = getdate(nowdate())
|
||||
|
||||
# if next data point is earler than the expected date
|
||||
# need to fill out missing data points
|
||||
while next_date > next_expected_date:
|
||||
# fill missing value
|
||||
result.append([next_expected_date, 0.0])
|
||||
next_expected_date = get_next_expected_date(next_expected_date, timegrain)
|
||||
|
||||
# add date for the last period (if missing)
|
||||
if result and get_period_ending(to_date, timegrain) > result[-1][0]:
|
||||
result.append([get_period_ending(to_date, timegrain), 0.0])
|
||||
data_index = 0
|
||||
if data:
|
||||
for i, d in enumerate(result):
|
||||
while data_index < len(data) and getdate(data[data_index][0]) <= d[0]:
|
||||
d[1] += data[data_index][1]
|
||||
data_index += 1
|
||||
|
||||
return result
|
||||
|
||||
|
|
@ -358,17 +299,12 @@ def get_period_ending(date, timegrain):
|
|||
return getdate(date)
|
||||
|
||||
def get_week_ending(date):
|
||||
# fun fact: week ends on the day before 1st Jan of the year.
|
||||
# for 2019 it is Monday
|
||||
# week starts on monday
|
||||
from datetime import timedelta
|
||||
start = date - timedelta(days = date.weekday())
|
||||
end = start + timedelta(days=6)
|
||||
|
||||
week_of_the_year = int(date.strftime('%U'))
|
||||
|
||||
if week_of_the_year == 52:
|
||||
date = add_to_date(date, years=1)
|
||||
# first day of next week
|
||||
date = add_to_date('{}-01-01'.format(date.year), weeks = (week_of_the_year%52) + 1)
|
||||
# last day of this week
|
||||
return add_to_date(date, days=-1)
|
||||
return end
|
||||
|
||||
def get_month_ending(date):
|
||||
month_of_the_year = int(date.strftime('%m'))
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ class TestDashboardChart(unittest.TestCase):
|
|||
self.assertEqual(get_period_ending('2019-04-10', 'Daily'),
|
||||
getdate('2019-04-10'))
|
||||
|
||||
# fun fact: week ends on the day before 1st Jan of the year.
|
||||
# for 2019 it is Monday
|
||||
# week starts on monday
|
||||
self.assertEqual(get_period_ending('2019-04-10', 'Weekly'),
|
||||
getdate('2019-04-15'))
|
||||
getdate('2019-04-14'))
|
||||
|
||||
self.assertEqual(get_period_ending('2019-04-10', 'Monthly'),
|
||||
getdate('2019-04-30'))
|
||||
|
|
@ -133,6 +132,34 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_weekly_dashboard_chart(self):
|
||||
insert_test_records()
|
||||
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Weekly Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Weekly Dashboard Chart')
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Weekly Dashboard Chart',
|
||||
chart_type = 'Sum',
|
||||
document_type = 'Communication',
|
||||
based_on = 'communication_date',
|
||||
value_based_on = 'rating',
|
||||
timespan = 'Select Date Range',
|
||||
time_interval = 'Weekly',
|
||||
from_date = datetime(2018, 12, 30),
|
||||
to_date = datetime(2019, 1, 15),
|
||||
filters_json = '[]',
|
||||
timeseries = 1
|
||||
)).insert()
|
||||
|
||||
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')
|
||||
|
|
@ -155,17 +182,16 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_dashboard_with_single_doctype(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Single DocType In Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Single DocType In Dashboard Chart')
|
||||
def insert_test_records():
|
||||
create_new_communication(datetime(2019, 1, 10), 100)
|
||||
create_new_communication(datetime(2019, 1, 6), 200)
|
||||
create_new_communication(datetime(2019, 1, 8), 300)
|
||||
|
||||
chart_doc = frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Single DocType In Dashboard Chart',
|
||||
chart_type = 'Count',
|
||||
document_type = 'System Settings',
|
||||
group_by_based_on = 'Created On',
|
||||
filters_json = '{}',
|
||||
))
|
||||
|
||||
self.assertRaises(frappe.ValidationError, chart_doc.insert)
|
||||
def create_new_communication(date, rating):
|
||||
communication = {
|
||||
'doctype': 'Communication',
|
||||
'subject': 'Test Communication',
|
||||
'rating': rating,
|
||||
'communication_date': date
|
||||
}
|
||||
frappe.get_doc(communication).insert()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@ class DeskPage(Document):
|
|||
if frappe.conf.developer_mode and self.is_standard:
|
||||
export_to_files(record_list=[['Desk Page', self.name]], record_module=self.module)
|
||||
|
||||
@staticmethod
|
||||
def get_module_page_map():
|
||||
filters = {
|
||||
'extends_another_page': 0,
|
||||
'for_user': '',
|
||||
}
|
||||
|
||||
pages = frappe.get_all("Desk Page", fields=["name", "module"], filters=filters, as_list=1)
|
||||
|
||||
return { page[1]: page[0] for page in pages }
|
||||
|
||||
def disable_saving_as_standard():
|
||||
return frappe.flags.in_install or \
|
||||
frappe.flags.in_patch or \
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ class TestEvent(unittest.TestCase):
|
|||
|
||||
self.assertEqual(set(json.loads(ev._assign)), set(["test@example.com", self.test_user]))
|
||||
|
||||
# close an assignment
|
||||
# Remove an assignment
|
||||
todo = frappe.get_doc("ToDo", {"reference_type": ev.doctype, "reference_name": ev.name,
|
||||
"owner": self.test_user})
|
||||
todo.status = "Closed"
|
||||
todo.status = "Cancelled"
|
||||
todo.save()
|
||||
|
||||
ev = frappe.get_doc("Event", ev.name)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('List View Setting', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "Prompt",
|
||||
"beta": 0,
|
||||
"creation": "2019-03-06 13:29:21.101860",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "disable_count",
|
||||
"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": "Disable Count",
|
||||
"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,
|
||||
"fieldname": "disable_sidebar_stats",
|
||||
"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": "Disable Sidebar Stats",
|
||||
"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,
|
||||
"fieldname": "disable_auto_refresh",
|
||||
"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": "Disable Auto Refresh",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-06 13:40:59.533586",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "List View Setting",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 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
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ListViewSetting(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('List View Settings', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2019-10-23 15:00:48.392374",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"disable_count",
|
||||
"disable_sidebar_stats",
|
||||
"disable_auto_refresh",
|
||||
"total_fields",
|
||||
"fields_html",
|
||||
"fields"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_count",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Count"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_sidebar_stats",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Sidebar Stats"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_auto_refresh",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Auto Refresh"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_fields",
|
||||
"fieldtype": "Select",
|
||||
"label": "Maximum Number of Fields",
|
||||
"options": "\n4\n5\n6\n7\n8\n9\n10"
|
||||
},
|
||||
{
|
||||
"fieldname": "fields_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "fields",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Fields",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-12 18:27:15.568199",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "List View Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
79
frappe/desk/doctype/list_view_settings/list_view_settings.py
Normal file
79
frappe/desk/doctype/list_view_settings/list_view_settings.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ListViewSettings(Document):
|
||||
|
||||
def on_update(self):
|
||||
frappe.clear_document_cache(self.doctype, self.name)
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_listview_settings(doctype, listview_settings, removed_listview_fields):
|
||||
|
||||
listview_settings = frappe.parse_json(listview_settings)
|
||||
removed_listview_fields = frappe.parse_json(removed_listview_fields)
|
||||
|
||||
if frappe.get_all("List View Settings", filters={"name": doctype}):
|
||||
doc = frappe.get_doc("List View Settings", doctype)
|
||||
doc.update(listview_settings)
|
||||
doc.save()
|
||||
else:
|
||||
doc = frappe.new_doc("List View Settings")
|
||||
doc.name = doctype
|
||||
doc.update(listview_settings)
|
||||
doc.insert()
|
||||
|
||||
set_listview_fields(doctype, listview_settings.get("fields"), removed_listview_fields)
|
||||
|
||||
return {
|
||||
"meta": frappe.get_meta(doctype, False),
|
||||
"listview_settings": doc
|
||||
}
|
||||
|
||||
def set_listview_fields(doctype, listview_fields, removed_listview_fields):
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
listview_fields = [f.get("fieldname") for f in frappe.parse_json(listview_fields) if f.get("fieldname")]
|
||||
|
||||
for field in removed_listview_fields:
|
||||
set_in_list_view_property(doctype, meta.get_field(field), "0")
|
||||
|
||||
for field in listview_fields:
|
||||
set_in_list_view_property(doctype, meta.get_field(field), "1")
|
||||
|
||||
def set_in_list_view_property(doctype, field, value):
|
||||
if not field or field.fieldname == "status_field":
|
||||
return
|
||||
|
||||
property_setter = frappe.db.get_value("Property Setter", {"doc_type": doctype, "field_name": field.fieldname, "property": "in_list_view"})
|
||||
if property_setter:
|
||||
doc = frappe.get_doc("Property Setter", property_setter)
|
||||
doc.value = value
|
||||
doc.save()
|
||||
else:
|
||||
frappe.make_property_setter({
|
||||
"doctype": doctype,
|
||||
"doctype_or_field": "DocField",
|
||||
"fieldname": field.fieldname,
|
||||
"property": "in_list_view",
|
||||
"value": value,
|
||||
"property_type": "Check"
|
||||
}, ignore_validate=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_listview_fields(doctype):
|
||||
meta = frappe.get_meta(doctype)
|
||||
path = frappe.get_module_path(frappe.scrub(meta.module), "doctype", frappe.scrub(meta.name), frappe.scrub(meta.name) + ".json")
|
||||
doctype_json = frappe.get_file_json(path)
|
||||
|
||||
fields = [f.get("fieldname") for f in doctype_json.get("fields") if f.get("in_list_view")]
|
||||
|
||||
if meta.title_field:
|
||||
if not meta.title_field.strip() in fields:
|
||||
fields.append(meta.title_field.strip())
|
||||
|
||||
return fields
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestListViewSetting(unittest.TestCase):
|
||||
class TestListViewSettings(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -3,10 +3,43 @@
|
|||
|
||||
frappe.ui.form.on('Notification Log', {
|
||||
refresh: function(frm) {
|
||||
let dt = frm.doc.document_type;
|
||||
let dn = frm.doc.document_name;
|
||||
frm.fields_dict.document_name.$input_wrapper
|
||||
.find('.control-value')
|
||||
.wrapInner(`<a href='#Form/${dt}/${dn}'></a>`);
|
||||
if (frm.doc.attached_file) {
|
||||
frm.trigger('set_attachment');
|
||||
} else {
|
||||
frm.get_field('attachment_link').$wrapper.empty();
|
||||
}
|
||||
},
|
||||
|
||||
open_reference_document: function(frm) {
|
||||
const dt = frm.doc.document_type;
|
||||
const dn = frm.doc.document_name;
|
||||
frappe.set_route('Form', dt, dn);
|
||||
},
|
||||
|
||||
set_attachment: function(frm) {
|
||||
const attachment = JSON.parse(frm.doc.attached_file);
|
||||
|
||||
const $wrapper = frm.get_field('attachment_link').$wrapper;
|
||||
$wrapper.html(`
|
||||
<div class="attached-file text-medium">
|
||||
<div class="ellipsis">
|
||||
<i class="fa fa-paperclip"></i>
|
||||
<a class="attached-file-link">${attachment.name}.pdf</a>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$wrapper.find(".attached-file-link").click(() => {
|
||||
const w = window.open(
|
||||
frappe.urllib.get_full_url(`/api/method/frappe.utils.print_format.download_pdf?
|
||||
doctype=${encodeURIComponent(attachment.doctype)}
|
||||
&name=${encodeURIComponent(attachment.name)}
|
||||
&format=${encodeURIComponent(attachment.print_format)}
|
||||
&lang=${encodeURIComponent(attachment.lang)}`)
|
||||
);
|
||||
if (!w) {
|
||||
frappe.msgprint(__("Please enable pop-ups"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2019-08-26 13:37:34.165254",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
|
|
@ -8,10 +9,12 @@
|
|||
"for_user",
|
||||
"type",
|
||||
"email_content",
|
||||
"column_break_4",
|
||||
"document_type",
|
||||
"read",
|
||||
"document_name",
|
||||
"attached_file",
|
||||
"attachment_link",
|
||||
"open_reference_document",
|
||||
"from_user"
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -20,57 +23,65 @@
|
|||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"read_only": 1
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "for_user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "For User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Type",
|
||||
"options": "Mention\nEnergy Point\nAssignment\nShare",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"options": "Mention\nEnergy Point\nAssignment\nShare\nAlert",
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "email_content",
|
||||
"fieldtype": "Text",
|
||||
"label": "Email Content",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Message",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "document_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Document Name",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"hidden": 1,
|
||||
"label": "Document Link",
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "From User",
|
||||
"options": "User",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -78,26 +89,51 @@
|
|||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Read"
|
||||
"label": "Read",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "open_reference_document",
|
||||
"fieldtype": "Button",
|
||||
"label": "Open Reference Document",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "attached_file",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Attached File",
|
||||
"options": "JSON",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "attachment_link",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Attachment Link",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"in_create": 1,
|
||||
"modified": "2019-11-12 15:22:35.283678",
|
||||
"links": [],
|
||||
"modified": "2020-05-31 22:31:12.886950",
|
||||
"modified_by": "umair@erpnext.com",
|
||||
"module": "Desk",
|
||||
"name": "Notification Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ def enqueue_create_notification(users, doc):
|
|||
|
||||
if isinstance(users, frappe.string_types):
|
||||
users = [user.strip() for user in users.split(',') if user.strip()]
|
||||
users = list(set(users))
|
||||
|
||||
frappe.enqueue(
|
||||
'frappe.desk.doctype.notification_log.notification_log.make_notification_logs',
|
||||
|
|
@ -58,6 +59,7 @@ def enqueue_create_notification(users, doc):
|
|||
|
||||
def make_notification_logs(doc, users):
|
||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
|
||||
|
||||
for user in users:
|
||||
if frappe.db.exists('User', user):
|
||||
if is_notifications_enabled(user):
|
||||
|
|
@ -68,7 +70,7 @@ def make_notification_logs(doc, users):
|
|||
_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':
|
||||
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point' or doc.type == 'Alert':
|
||||
_doc.insert(ignore_permissions=True)
|
||||
|
||||
def send_notification_email(doc):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2019-09-11 22:15:44.851526",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -21,52 +22,68 @@
|
|||
"default": "1",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
"label": "Enabled",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscribed_documents",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Subscribed Documents",
|
||||
"options": "Notification Subscribed Document"
|
||||
"options": "Notification Subscribed Document",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Email Settings"
|
||||
"label": "Email Settings",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "enable_email_notifications",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Email Notifications"
|
||||
"label": "Enable Email Notifications",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_email_notifications",
|
||||
"fieldname": "enable_email_mention",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mentions"
|
||||
"label": "Mentions",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_email_notifications",
|
||||
"fieldname": "enable_email_assignment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Assignments"
|
||||
"label": "Assignments",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_email_notifications",
|
||||
"fieldname": "enable_email_energy_point",
|
||||
"fieldtype": "Check",
|
||||
"label": "Energy Points"
|
||||
"label": "Energy Points",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_email_notifications",
|
||||
"fieldname": "enable_email_share",
|
||||
"fieldtype": "Check",
|
||||
"label": "Document Share"
|
||||
"label": "Document Share",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "__user",
|
||||
|
|
@ -75,18 +92,23 @@
|
|||
"hidden": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "seen",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Seen"
|
||||
"label": "Seen",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-11-19 12:57:59.356786",
|
||||
"links": [],
|
||||
"modified": "2020-05-31 22:16:40.798019",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Notification Settings",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ def is_email_notifications_enabled_for_type(user, notification_type):
|
|||
if not is_email_notifications_enabled(user):
|
||||
return False
|
||||
|
||||
if notification_type == 'Alert':
|
||||
return False
|
||||
|
||||
fieldname = 'enable_email_' + frappe.scrub(notification_type)
|
||||
enabled = frappe.db.get_value('Notification Settings', user, fieldname)
|
||||
if enabled is None:
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ToDo(Document):
|
|||
filters={
|
||||
"reference_type": self.reference_type,
|
||||
"reference_name": self.reference_name,
|
||||
"status": "Open"
|
||||
"status": ("!=", "Cancelled")
|
||||
},
|
||||
fields=["owner"], as_list=True)]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_submitted_linked_docs(doctype, name, docs=None):
|
||||
def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
|
||||
"""
|
||||
Get all nested submitted linked doctype linkinfo
|
||||
|
||||
|
|
@ -31,12 +31,26 @@ def get_submitted_linked_docs(doctype, name, docs=None):
|
|||
if not docs:
|
||||
docs = []
|
||||
|
||||
if not linked:
|
||||
linked = {}
|
||||
|
||||
linkinfo = get_linked_doctypes(doctype)
|
||||
linked_docs = get_linked_docs(doctype, name, linkinfo)
|
||||
|
||||
link_count = 0
|
||||
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)
|
||||
|
||||
|
|
@ -47,7 +61,7 @@ def get_submitted_linked_docs(doctype, name, docs=None):
|
|||
if link.name in [doc.get("name") for doc in docs]:
|
||||
continue
|
||||
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs)
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs, linked)
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
|
|
|
|||
|
|
@ -7,17 +7,16 @@ import frappe
|
|||
@frappe.whitelist()
|
||||
def get_list_settings(doctype):
|
||||
try:
|
||||
return frappe.get_cached_doc("List View Setting", doctype)
|
||||
return frappe.get_cached_doc("List View Settings", doctype)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.clear_messages()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_list_settings(doctype, values):
|
||||
try:
|
||||
doc = frappe.get_doc("List View Setting", doctype)
|
||||
doc = frappe.get_doc("List View Settings", doctype)
|
||||
except frappe.DoesNotExistError:
|
||||
doc = frappe.new_doc("List View Setting")
|
||||
doc = frappe.new_doc("List View Settings")
|
||||
doc.name = doctype
|
||||
frappe.clear_messages()
|
||||
doc.update(frappe.parse_json(values))
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ def get_open_count(doctype, name, items=[]):
|
|||
continue
|
||||
|
||||
filters = get_filters_for(d)
|
||||
fieldname = links.get("non_standard_fieldnames", {}).get(d, links.fieldname)
|
||||
fieldname = links.get("non_standard_fieldnames", {}).get(d, links.get('fieldname'))
|
||||
data = {"name": d}
|
||||
if filters:
|
||||
# get the fieldname for the current document
|
||||
|
|
|
|||
|
|
@ -110,7 +110,11 @@ class UserProfile {
|
|||
|
||||
|
||||
render_line_chart() {
|
||||
this.line_chart_filters = [['Energy Point Log', 'user', '=', this.user_id, false]];
|
||||
this.line_chart_filters = [
|
||||
['Energy Point Log', 'user', '=', this.user_id, false],
|
||||
['Energy Point Log', 'type', '!=', 'Review', false]
|
||||
];
|
||||
|
||||
this.line_chart_config = {
|
||||
timespan: 'Last Month',
|
||||
time_interval: 'Daily',
|
||||
|
|
@ -186,7 +190,10 @@ class UserProfile {
|
|||
options: ['All', 'Auto', 'Criticism', 'Appreciation', 'Revert'],
|
||||
action: (selected_item) => {
|
||||
if (selected_item === 'All') {
|
||||
if (this.line_chart_filters.length > 1) this.line_chart_filters.pop();
|
||||
this.line_chart_filters = [
|
||||
['Energy Point Log', 'user', '=', this.user_id, false],
|
||||
['Energy Point Log', 'type', '!=', 'Review', false]
|
||||
];
|
||||
} else {
|
||||
this.line_chart_filters[1] = ['Energy Point Log', 'type', '=', selected_item, false];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,8 +62,16 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
|
|||
ljust_list(res, 6)
|
||||
|
||||
if report.custom_columns:
|
||||
# Original query columns, needed to reorder data as per custom columns
|
||||
query_columns = columns
|
||||
# Reordered columns
|
||||
columns = json.loads(report.custom_columns)
|
||||
|
||||
if report.report_type == 'Query Report':
|
||||
result = reorder_data_for_custom_columns(columns, query_columns, result)
|
||||
|
||||
result = add_data_to_custom_columns(columns, result)
|
||||
|
||||
if custom_columns:
|
||||
result = add_data_to_custom_columns(custom_columns, result)
|
||||
|
||||
|
|
@ -208,6 +216,23 @@ def add_data_to_custom_columns(columns, result):
|
|||
|
||||
return data
|
||||
|
||||
def reorder_data_for_custom_columns(custom_columns, columns, result):
|
||||
reordered_result = []
|
||||
columns = [col.split(":")[0] for col in columns]
|
||||
|
||||
for res in result:
|
||||
r = []
|
||||
for col in custom_columns:
|
||||
try:
|
||||
idx = columns.index(col.get("label"))
|
||||
r.append(res[idx])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
reordered_result.append(r)
|
||||
|
||||
return reordered_result
|
||||
|
||||
def get_prepared_report_result(report, filters, dn="", user=None):
|
||||
latest_report_data = {}
|
||||
doc = None
|
||||
|
|
|
|||
|
|
@ -216,8 +216,10 @@ def send_daily():
|
|||
elif auto_email_report.frequency == 'Weekly':
|
||||
if auto_email_report.day_of_week != current_day:
|
||||
continue
|
||||
|
||||
auto_email_report.send()
|
||||
try:
|
||||
auto_email_report.send()
|
||||
except Exception as e:
|
||||
frappe.log_error(e, _('Failed to send {0} Auto Email Report').format(auto_email_report.name))
|
||||
|
||||
|
||||
def send_monthly():
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"default_incoming",
|
||||
"email_sync_option",
|
||||
"initial_sync_count",
|
||||
"create_contact",
|
||||
"section_break_12",
|
||||
"enable_automatic_linking",
|
||||
"section_break_13",
|
||||
|
|
@ -114,9 +115,9 @@
|
|||
"depends_on": "eval:!doc.service",
|
||||
"fieldname": "domain",
|
||||
"fieldtype": "Link",
|
||||
"label": "Domain",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Domain",
|
||||
"options": "Email Domain"
|
||||
},
|
||||
{
|
||||
|
|
@ -408,11 +409,17 @@
|
|||
"fieldname": "use_ssl_for_outgoing",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use SSL for Outgoing"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_contact",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Contacts from Incoming Emails"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-inbox",
|
||||
"links": [],
|
||||
"modified": "2020-04-06 19:20:50.491146",
|
||||
"modified": "2020-05-11 15:18:43.931499",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
@ -427,11 +434,11 @@
|
|||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Inbox User"
|
||||
"read": 1,
|
||||
"role": "Inbox User"
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ frappe.ui.form.on("Notification", {
|
|||
});
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frm.toggle_reqd("recipients", frm.doc.channel=="Email");
|
||||
frappe.notification.setup_fieldname_select(frm);
|
||||
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
|
||||
frm.trigger('event');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2014-07-11 17:18:09.923399",
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
"days_in_advance",
|
||||
"value_changed",
|
||||
"sender",
|
||||
"send_system_notification",
|
||||
"sender_email",
|
||||
"section_break_9",
|
||||
"condition",
|
||||
|
|
@ -46,32 +48,43 @@
|
|||
"default": "1",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
"label": "Enabled",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Email",
|
||||
"depends_on": "eval: !doc.disable_channel",
|
||||
"fieldname": "channel",
|
||||
"fieldtype": "Select",
|
||||
"label": "Channel",
|
||||
"options": "Email\nSlack",
|
||||
"options": "Email\nSlack\nSystem Notification",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.channel=='Slack'",
|
||||
"fieldname": "slack_webhook_url",
|
||||
"fieldtype": "Link",
|
||||
"label": "Slack Channel",
|
||||
"options": "Slack Webhook URL"
|
||||
"mandatory_depends_on": "eval:doc.channel=='Slack'",
|
||||
"options": "Slack Webhook URL",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "filters",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Filters"
|
||||
"label": "Filters",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"description": "To add dynamic subject, use jinja tags like\n\n<div><pre><code>{{ doc.name }} Delivered</code></pre></div>",
|
||||
|
|
@ -80,7 +93,9 @@
|
|||
"ignore_xss_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
|
|
@ -90,13 +105,17 @@
|
|||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard"
|
||||
"label": "Is Standard",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_standard",
|
||||
|
|
@ -104,11 +123,15 @@
|
|||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Module",
|
||||
"options": "Module Def"
|
||||
"options": "Module Def",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "event",
|
||||
|
|
@ -117,21 +140,27 @@
|
|||
"label": "Send Alert On",
|
||||
"options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nMethod\nCustom",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.event=='Method'",
|
||||
"description": "Trigger on valid methods like \"before_insert\", \"after_update\", etc (will depend on the DocType selected)",
|
||||
"fieldname": "method",
|
||||
"fieldtype": "Data",
|
||||
"label": "Trigger Method"
|
||||
"label": "Trigger Method",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.event==\"Days After\" || doc.event==\"Days Before\"",
|
||||
"description": "Send alert if date matches this field's value",
|
||||
"fieldname": "date_changed",
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Date"
|
||||
"label": "Reference Date",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -139,31 +168,41 @@
|
|||
"description": "Send days before or after the reference date",
|
||||
"fieldname": "days_in_advance",
|
||||
"fieldtype": "Int",
|
||||
"label": "Days Before or After"
|
||||
"label": "Days Before or After",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.event==\"Value Change\"",
|
||||
"description": "Send alert if this field's value changes",
|
||||
"fieldname": "value_changed",
|
||||
"fieldtype": "Select",
|
||||
"label": "Value Changed"
|
||||
"label": "Value Changed",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sender",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sender",
|
||||
"options": "Email Account"
|
||||
"options": "Email Account",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sender_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"description": "Optional: The alert will be sent if this expression is true",
|
||||
|
|
@ -171,99 +210,143 @@
|
|||
"fieldtype": "Code",
|
||||
"ignore_xss_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Condition"
|
||||
"label": "Condition",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "html_7",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total > 40000\n</pre>\n"
|
||||
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total > 40000\n</pre>\n",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "property_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Set Property After Alert"
|
||||
"label": "Set Property After Alert",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "set_property_after_alert",
|
||||
"fieldtype": "Select",
|
||||
"label": "Set Property After Alert"
|
||||
"label": "Set Property After Alert",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "property_value",
|
||||
"fieldtype": "Data",
|
||||
"label": "Value To Be Set"
|
||||
"label": "Value To Be Set",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.channel=='Email'",
|
||||
"depends_on": "eval:doc.channel!=='Slack'",
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Recipients"
|
||||
"label": "Recipients",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "recipients",
|
||||
"fieldtype": "Table",
|
||||
"label": "Recipients",
|
||||
"options": "Notification Recipient"
|
||||
"mandatory_depends_on": "eval:doc.channel!=='Slack'",
|
||||
"options": "Notification Recipient",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "message_sb",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Message"
|
||||
"label": "Message",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Add your message here",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Code",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Message"
|
||||
"label": "Message",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.channel=='Email'",
|
||||
"fieldname": "message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Message Examples",
|
||||
"options": "<h5>Message Example</h5>\n\n<pre><h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n</pre>"
|
||||
"options": "<h5>Message Example</h5>\n\n<pre><h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n</pre>",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.channel=='Slack'",
|
||||
"fieldname": "slack_message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Message Examples",
|
||||
"options": "<h5>Message Example</h5>\n\n<pre>*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n</pre>"
|
||||
"options": "<h5>Message Example</h5>\n\n<pre>*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n</pre>",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "view_properties",
|
||||
"fieldtype": "Button",
|
||||
"label": "View Properties (via Customize Form)"
|
||||
"label": "View Properties (via Customize Form)",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "attach_print",
|
||||
"fieldname": "column_break_25",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
"label": "Print Settings",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "attach_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Attach Print"
|
||||
"label": "Attach Print",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "attach_print",
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Format",
|
||||
"options": "Print Format"
|
||||
"options": "Print Format",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.channel !== 'System Notification'",
|
||||
"description": "If enabled, the notification will show up in the notifications dropdown on the top right corner of the navigation bar.",
|
||||
"fieldname": "send_system_notification",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send System Notification",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-envelope",
|
||||
"modified": "2019-07-15 13:17:02.585013",
|
||||
"links": [],
|
||||
"modified": "2020-05-29 16:03:10.914526",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Notification",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from frappe.utils.jinja import validate_template
|
|||
from frappe.modules.utils import export_module_json, get_doc_module
|
||||
from six import string_types
|
||||
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
|
||||
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification
|
||||
|
||||
class Notification(Document):
|
||||
def onload(self):
|
||||
|
|
@ -125,6 +126,9 @@ def get_context(context):
|
|||
if self.channel == 'Slack':
|
||||
self.send_a_slack_msg(doc, context)
|
||||
|
||||
if self.channel == 'System Notification' or self.send_system_notification:
|
||||
self.create_system_notification(doc, context)
|
||||
|
||||
if self.set_property_after_alert:
|
||||
allow_update = True
|
||||
if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
|
||||
|
|
@ -143,6 +147,25 @@ def get_context(context):
|
|||
except Exception:
|
||||
frappe.log_error(title='Document update failed', message=frappe.get_traceback())
|
||||
|
||||
def create_system_notification(self, doc, context):
|
||||
subject = self.subject
|
||||
if "{" in subject:
|
||||
subject = frappe.render_template(self.subject, context)
|
||||
|
||||
attachments = self.get_attachment(doc)
|
||||
recipients, cc, bcc = self.get_list_of_recipients(doc, context)
|
||||
users = recipients + cc + bcc
|
||||
|
||||
notification_doc = {
|
||||
'type': 'Alert',
|
||||
'document_type': doc.doctype,
|
||||
'document_name': doc.name,
|
||||
'subject': subject,
|
||||
'email_content': frappe.render_template(self.message, context),
|
||||
'attached_file': attachments and json.dumps(attachments[0])
|
||||
}
|
||||
enqueue_create_notification(users, notification_doc)
|
||||
|
||||
def send_an_email(self, doc, context):
|
||||
from email.utils import formataddr
|
||||
subject = self.subject
|
||||
|
|
@ -228,8 +251,7 @@ def get_context(context):
|
|||
|
||||
# ignoring attachment as draft and cancelled documents are not allowed to print
|
||||
status = "Draft" if doc.docstatus == 0 else "Cancelled"
|
||||
frappe.throw(_("""Not allowed to attach {0} document,
|
||||
please enable Allow Print For {0} in Print Settings""".format(status)),
|
||||
frappe.throw(_("""Not allowed to attach {0} document, please enable Allow Print For {0} in Print Settings""").format(status),
|
||||
title=_("Error in Notification"))
|
||||
else:
|
||||
return [{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ if sys.version_info.major == 2:
|
|||
else:
|
||||
from builtins import FileNotFoundError
|
||||
|
||||
class SiteNotSpecifiedError(Exception):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = "Please specify --site sitename"
|
||||
super(Exception, self).__init__(self.message)
|
||||
|
||||
class ValidationError(Exception):
|
||||
http_status_code = 417
|
||||
|
||||
|
|
@ -98,6 +103,7 @@ class InvalidColumnName(ValidationError): pass
|
|||
class IncompatibleApp(ValidationError): pass
|
||||
class InvalidDates(ValidationError): pass
|
||||
class DataTooLongException(ValidationError): pass
|
||||
class FileAlreadyAttachedException(Exception): pass
|
||||
# OAuth exceptions
|
||||
class InvalidAuthorizationHeader(CSRFTokenError): pass
|
||||
class InvalidAuthorizationPrefix(CSRFTokenError): pass
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ website_route_rules = [
|
|||
{"from_route": "/profile", "to_route": "me"},
|
||||
]
|
||||
|
||||
base_template = "templates/base.html"
|
||||
|
||||
write_file_keys = ["file_url", "file_name"]
|
||||
|
||||
notification_config = "frappe.core.notifications.get_notification_config"
|
||||
|
|
@ -270,7 +272,10 @@ setup_wizard_exception = [
|
|||
]
|
||||
|
||||
before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute']
|
||||
after_migrate = ['frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist']
|
||||
after_migrate = [
|
||||
'frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist',
|
||||
'frappe.modules.full_text_search.build_index_for_all_routes'
|
||||
]
|
||||
|
||||
otp_methods = ['OTP App','Email','SMS']
|
||||
user_privacy_documents = [
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ def make_site_dirs():
|
|||
os.path.join(site_private_path, 'backups'),
|
||||
os.path.join(site_public_path, 'files'),
|
||||
os.path.join(site_private_path, 'files'),
|
||||
os.path.join(frappe.local.site_path, 'logs'),
|
||||
os.path.join(frappe.local.site_path, 'task-logs')):
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
|
@ -298,7 +299,8 @@ def remove_missing_apps():
|
|||
|
||||
def extract_sql_gzip(sql_gz_path):
|
||||
try:
|
||||
subprocess.check_call(['gzip', '-d', '-v', '-f', sql_gz_path])
|
||||
# kdvf - keep, decompress, verbose, force
|
||||
subprocess.check_call(['gzip', '-kdvf', sql_gz_path])
|
||||
except:
|
||||
raise
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True):
|
|||
did_not_upload, error_log = backup_to_dropbox(upload_db_backup)
|
||||
if did_not_upload: raise Exception
|
||||
|
||||
send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to")
|
||||
if cint(frappe.db.get_value("Dropbox Settings", None, "send_email_for_successful_backup")):
|
||||
send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to")
|
||||
except JobTimeoutException:
|
||||
if retry_count < 2:
|
||||
args = {
|
||||
|
|
@ -90,7 +91,7 @@ def backup_to_dropbox(upload_db_backup=True):
|
|||
dropbox_settings['access_token'] = access_token['oauth2_token']
|
||||
set_dropbox_access_token(access_token['oauth2_token'])
|
||||
|
||||
dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'])
|
||||
dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'], timeout=None)
|
||||
|
||||
if upload_db_backup:
|
||||
if frappe.flags.create_new_backup:
|
||||
|
|
|
|||
|
|
@ -147,11 +147,14 @@ def sync_contacts_from_google_contacts(g_contact):
|
|||
results = []
|
||||
contacts_updated = 0
|
||||
|
||||
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
|
||||
contacts = frappe._dict()
|
||||
|
||||
while True:
|
||||
try:
|
||||
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
|
||||
contacts = google_contacts.people().connections().list(resourceName='people/me',syncToken=sync_token,
|
||||
personFields="names,emailAddresses,organizations,phoneNumbers").execute()
|
||||
contacts = google_contacts.people().connections().list(resourceName='people/me', pageToken=contacts.get("nextPageToken"),
|
||||
syncToken=sync_token, pageSize=2000, requestSyncToken=True, personFields="names,emailAddresses,organizations,phoneNumbers").execute()
|
||||
|
||||
except HttpError as err:
|
||||
frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts {0}, error code {1}.").format(account.name, err.resp.status))
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe import _
|
||||
import json
|
||||
import hmac
|
||||
import hashlib
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_url, call_hook_method, cint, get_timestamp
|
||||
|
|
@ -317,6 +319,20 @@ class RazorpaySettings(Document):
|
|||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
|
||||
def verify_signature(self, body, signature, key):
|
||||
key = bytes(key, 'utf-8')
|
||||
body = bytes(body, 'utf-8')
|
||||
|
||||
dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256)
|
||||
|
||||
generated_signature = dig.hexdigest()
|
||||
result = hmac.compare_digest(generated_signature, signature)
|
||||
|
||||
if not result:
|
||||
frappe.throw(_('Razorpay Signature Verification Failed'), exc=frappe.PermissionError)
|
||||
|
||||
return result
|
||||
|
||||
def capture_payment(is_sandbox=False, sanbox_response=None):
|
||||
"""
|
||||
Verifies the purchase as complete by the merchant.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato
|
|||
|
||||
def migrate_to(local_site, frappe_provider):
|
||||
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
|
||||
frappe_provider = "frappecloud.com"
|
||||
return frappecloud_migrator(local_site, frappe_provider)
|
||||
else:
|
||||
print("{} is not supported yet".format(frappe_provider))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,129 @@ import requests
|
|||
import frappe
|
||||
import frappe.utils.backups
|
||||
from frappe.utils import get_installed_apps_info
|
||||
from frappe.utils.commands import render_table, add_line_after
|
||||
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
|
||||
|
||||
|
||||
def get_new_site_options():
|
||||
|
|
@ -46,21 +168,6 @@ def is_subdomain_available(subdomain):
|
|||
return available
|
||||
|
||||
|
||||
def render_plan_table(plans_list):
|
||||
plans_table = []
|
||||
|
||||
# title row
|
||||
visible_headers = ["name", "cpu_time_per_day"]
|
||||
plans_table.append(["Plan", "CPU Time"])
|
||||
|
||||
# all rows
|
||||
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)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def choose_plan(plans_list):
|
||||
print("{} plans available".format(len(plans_list)))
|
||||
|
|
@ -113,19 +220,6 @@ def check_app_compat(available_group):
|
|||
return is_compat, filtered_apps
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def filter_apps(app_groups):
|
||||
render_group_table(app_groups)
|
||||
|
|
@ -148,24 +242,6 @@ def filter_apps(app_groups):
|
|||
|
||||
return selected_group["name"], filtered_apps
|
||||
|
||||
@add_line_after
|
||||
def create_session():
|
||||
# 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! ✅")
|
||||
session.headers.update({"X-Press-Team": username})
|
||||
return session
|
||||
else:
|
||||
print("Authorization Failed with Error Code {}".format(login_sc.status_code))
|
||||
|
||||
|
||||
@add_line_after
|
||||
def get_subdomain(domain):
|
||||
|
|
@ -208,61 +284,114 @@ def upload_backup(local_site):
|
|||
return files_uploaded
|
||||
|
||||
|
||||
def frappecloud_migrator(local_site, remote_site):
|
||||
global login_url, upload_url, files_url, options_url, site_exists_url, session
|
||||
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 })
|
||||
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
|
||||
|
||||
remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
|
||||
|
||||
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)
|
||||
|
||||
print("Frappe Cloud credentials @ {}".format(remote_site))
|
||||
migrator_actions = [
|
||||
{ "title": "Create a new site", "fn": new_site },
|
||||
{ "title": "Restore to an existing site", "fn": restore_site }
|
||||
]
|
||||
|
||||
# get credentials + auth user + start session
|
||||
session = create_session()
|
||||
|
||||
if session:
|
||||
# connect to site db
|
||||
frappe.init(site=local_site)
|
||||
frappe.connect()
|
||||
# available actions defined in migrator_actions
|
||||
primary_action = select_primary_action()
|
||||
|
||||
# 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)
|
||||
frappe.destroy()
|
||||
|
||||
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:
|
||||
print("Request failed with error code {}".format(site_creation_request.status_code))
|
||||
reason = html2text(site_creation_request.text)
|
||||
print(reason)
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
sys.exit(1)
|
||||
primary_action(local_site)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ data_fieldtypes = (
|
|||
'Signature',
|
||||
'Color',
|
||||
'Barcode',
|
||||
'Geolocation'
|
||||
'Geolocation',
|
||||
'Duration'
|
||||
)
|
||||
|
||||
no_value_fields = ('Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect', 'Button', 'Image',
|
||||
|
|
|
|||
|
|
@ -693,7 +693,7 @@ class BaseDocument(object):
|
|||
df = self.meta.get_field(fieldname)
|
||||
sanitized_value = value
|
||||
|
||||
if df and df.get("fieldtype") in ("Data", "Code", "Small Text") and df.get("options")=="Email":
|
||||
if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email":
|
||||
sanitized_value = sanitize_email(value)
|
||||
|
||||
elif df and (df.get("ignore_xss_filter")
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import frappe, json, copy, re
|
|||
from frappe.model import optional_fields
|
||||
from frappe.client import check_parent_permission
|
||||
from frappe.model.utils.user_settings import get_user_settings, update_user_settings
|
||||
from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, nowdate
|
||||
from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range
|
||||
from frappe.model.meta import get_table_columns
|
||||
|
||||
class DatabaseQuery(object):
|
||||
|
|
@ -354,7 +354,9 @@ class DatabaseQuery(object):
|
|||
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
|
||||
"""
|
||||
|
||||
f = get_filter(self.doctype, f)
|
||||
from frappe.boot import get_additional_filters_from_hooks
|
||||
additional_filters_config = get_additional_filters_from_hooks()
|
||||
f = get_filter(self.doctype, f, additional_filters_config)
|
||||
|
||||
tname = ('`tab' + f.doctype + '`')
|
||||
if not tname in self.tables:
|
||||
|
|
@ -368,6 +370,9 @@ class DatabaseQuery(object):
|
|||
|
||||
can_be_null = True
|
||||
|
||||
if f.operator.lower() in additional_filters_config:
|
||||
f.update(get_additional_filter_field(additional_filters_config, f, f.value))
|
||||
|
||||
# prepare in condition
|
||||
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'):
|
||||
values = f.value or ''
|
||||
|
|
@ -426,29 +431,8 @@ class DatabaseQuery(object):
|
|||
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
|
||||
can_be_null = False
|
||||
|
||||
if f.operator.lower() in ('previous', 'next'):
|
||||
if f.operator.lower() == "previous":
|
||||
if f.value == "1 week":
|
||||
date_range = [add_to_date(nowdate(), days=-7), nowdate()]
|
||||
elif f.value == "1 month":
|
||||
date_range = [add_to_date(nowdate(), months=-1), nowdate()]
|
||||
elif f.value == "3 months":
|
||||
date_range = [add_to_date(nowdate(), months=-3), nowdate()]
|
||||
elif f.value == "6 months":
|
||||
date_range = [add_to_date(nowdate(), months=-6), nowdate()]
|
||||
elif f.value == "1 year":
|
||||
date_range = [add_to_date(nowdate(), years=-1), nowdate()]
|
||||
elif f.operator.lower() == "next":
|
||||
if f.value == "1 week":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), days=7)]
|
||||
elif f.value == "1 month":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), months=1)]
|
||||
elif f.value == "3 months":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), months=3)]
|
||||
elif f.value == "6 months":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), months=6)]
|
||||
elif f.value == "1 year":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), years=1)]
|
||||
if f.operator.lower() in ('previous', 'next', 'timespan'):
|
||||
date_range = get_date_range(f.operator.lower(), f.value)
|
||||
f.operator = "Between"
|
||||
f.value = date_range
|
||||
fallback = "'0001-01-01 00:00:00'"
|
||||
|
|
@ -843,4 +827,31 @@ def get_between_date_filter(value, df=None):
|
|||
frappe.db.format_date(from_date),
|
||||
frappe.db.format_date(to_date))
|
||||
|
||||
return data
|
||||
return data
|
||||
|
||||
def get_additional_filter_field(additional_filters_config, f, value):
|
||||
additional_filter = additional_filters_config[f.operator.lower()]
|
||||
f = frappe._dict(frappe.get_attr(additional_filter['get_field'])())
|
||||
if f.query_value:
|
||||
for option in f.options:
|
||||
option = frappe._dict(option)
|
||||
if option.value == value:
|
||||
f.value = option.query_value
|
||||
return f
|
||||
|
||||
def get_date_range(operator, value):
|
||||
timespan_map = {
|
||||
'1 week': 'week',
|
||||
'1 month': 'month',
|
||||
'3 months': 'quarter',
|
||||
'6 months': '6 months',
|
||||
'1 year': 'year',
|
||||
}
|
||||
period_map = {
|
||||
'previous': 'last',
|
||||
'next': 'next',
|
||||
}
|
||||
|
||||
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
|
||||
|
||||
return get_timespan_date_range(timespan)
|
||||
|
|
@ -14,6 +14,12 @@ def make_mapped_doc(method, source_name, selected_children=None, args=None):
|
|||
Sets selected_children as flags for the `get_mapped_doc` method.
|
||||
|
||||
Called from `open_mapped_doc` from create_new.js'''
|
||||
|
||||
for hook in frappe.get_hooks("override_whitelisted_methods", {}).get(method, []):
|
||||
# override using the first hook
|
||||
method = hook
|
||||
break
|
||||
|
||||
method = frappe.get_attr(method)
|
||||
|
||||
if method not in frappe.whitelisted:
|
||||
|
|
|
|||
|
|
@ -437,7 +437,7 @@ class Meta(Document):
|
|||
|
||||
if not self.custom:
|
||||
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
|
||||
data = frappe.get_attr(hook)(data=data)
|
||||
data = frappe._dict(frappe.get_attr(hook)(data=data))
|
||||
|
||||
return data
|
||||
|
||||
|
|
@ -483,6 +483,9 @@ class Meta(Document):
|
|||
def get_row_template(self):
|
||||
return self.get_web_template(suffix='_row')
|
||||
|
||||
def get_list_template(self):
|
||||
return self.get_web_template(suffix='_list')
|
||||
|
||||
def get_web_template(self, suffix=''):
|
||||
'''Returns the relative path of the row template for this doctype'''
|
||||
module_name = frappe.scrub(self.module)
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
|
|||
|
||||
if not merge:
|
||||
rename_parent_and_child(doctype, old, new, meta)
|
||||
else:
|
||||
update_assignments(old, new, doctype)
|
||||
|
||||
# update link fields' values
|
||||
link_fields = get_link_fields(doctype)
|
||||
|
|
@ -104,6 +106,27 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
|
|||
|
||||
return new
|
||||
|
||||
def update_assignments(old, new, doctype):
|
||||
old_assignments = frappe.parse_json(frappe.db.get_value(doctype, old, '_assign')) or []
|
||||
new_assignments = frappe.parse_json(frappe.db.get_value(doctype, new, '_assign')) or []
|
||||
common_assignments = list(set(old_assignments).intersection(new_assignments))
|
||||
|
||||
for user in common_assignments:
|
||||
# delete todos linked to old doc
|
||||
todos = frappe.db.get_all('ToDo',
|
||||
{
|
||||
'owner': user,
|
||||
'reference_type': doctype,
|
||||
'reference_name': old,
|
||||
},
|
||||
['name', 'description']
|
||||
)
|
||||
|
||||
for todo in todos:
|
||||
frappe.delete_doc('ToDo', todo.name)
|
||||
|
||||
unique_assignments = list(set(old_assignments + new_assignments))
|
||||
frappe.db.set_value(doctype, new, '_assign', frappe.as_json(unique_assignments, indent=0))
|
||||
|
||||
def update_user_settings(old, new, link_fields):
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ def set_workflow_state_on_action(doc, workflow_name, action):
|
|||
return
|
||||
|
||||
action_map = {
|
||||
'update_after_submit': '1',
|
||||
'submit': '1',
|
||||
'cancel': '2'
|
||||
}
|
||||
|
|
|
|||
106
frappe/modules/full_text_search.py
Normal file
106
frappe/modules/full_text_search.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from whoosh.index import create_in, open_dir
|
||||
from whoosh.fields import TEXT, ID, Schema
|
||||
from whoosh.qparser import MultifieldParser, FieldsPlugin, WildcardPlugin
|
||||
from whoosh.query import Prefix
|
||||
from bs4 import BeautifulSoup
|
||||
from frappe.website.render import render_page
|
||||
from frappe.utils import set_request, cint
|
||||
from frappe.utils.global_search import get_routes_to_index
|
||||
|
||||
|
||||
def build_index_for_all_routes():
|
||||
print("Building search index for all web routes...")
|
||||
routes = get_routes_to_index()
|
||||
documents = [get_document_to_index(route) for route in routes]
|
||||
build_index("web_routes", documents)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def web_search(index_name, query, scope=None, limit=20):
|
||||
limit = cint(limit)
|
||||
return search(index_name, query, scope, limit)
|
||||
|
||||
|
||||
def get_document_to_index(route):
|
||||
frappe.set_user("Guest")
|
||||
frappe.local.no_cache = True
|
||||
|
||||
try:
|
||||
set_request(method="GET", path=route)
|
||||
content = render_page(route)
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
page_content = soup.find(class_="page_content")
|
||||
text_content = page_content.text if page_content else ""
|
||||
title = soup.title.text.strip() if soup.title else route
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
return frappe._dict(title=title, content=text_content, path=route)
|
||||
except (
|
||||
frappe.PermissionError,
|
||||
frappe.DoesNotExistError,
|
||||
frappe.ValidationError,
|
||||
Exception,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def build_index(index_name, documents):
|
||||
schema = Schema(
|
||||
title=TEXT(stored=True), path=ID(stored=True), content=TEXT(stored=True)
|
||||
)
|
||||
|
||||
index_dir = get_index_path(index_name)
|
||||
frappe.create_folder(index_dir)
|
||||
|
||||
ix = create_in(index_dir, schema)
|
||||
writer = ix.writer()
|
||||
|
||||
for document in documents:
|
||||
if document:
|
||||
writer.add_document(
|
||||
title=document.title, path=document.path, content=document.content
|
||||
)
|
||||
|
||||
writer.commit()
|
||||
|
||||
|
||||
def search(index_name, text, scope=None, limit=20):
|
||||
index_dir = get_index_path(index_name)
|
||||
ix = open_dir(index_dir)
|
||||
|
||||
results = None
|
||||
out = []
|
||||
with ix.searcher() as searcher:
|
||||
parser = MultifieldParser(["title", "content"], ix.schema)
|
||||
parser.remove_plugin_class(FieldsPlugin)
|
||||
parser.remove_plugin_class(WildcardPlugin)
|
||||
query = parser.parse(text)
|
||||
|
||||
filter_scoped = None
|
||||
if scope:
|
||||
filter_scoped = Prefix("path", scope)
|
||||
results = searcher.search(query, limit=limit, filter=filter_scoped)
|
||||
|
||||
for r in results:
|
||||
title_highlights = r.highlights("title")
|
||||
content_highlights = r.highlights("content")
|
||||
out.append(
|
||||
frappe._dict(
|
||||
title=r["title"],
|
||||
path=r["path"],
|
||||
title_highlights=title_highlights,
|
||||
content_highlights=content_highlights,
|
||||
)
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def get_index_path(index_name):
|
||||
return frappe.get_site_path("indexes", index_name)
|
||||
|
|
@ -278,6 +278,14 @@ frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
|
|||
frappe.patches.v13_0.migrate_translation_column_data
|
||||
frappe.patches.v13_0.set_read_times
|
||||
frappe.patches.v13_0.remove_web_view
|
||||
frappe.patches.v13_0.site_wise_logging
|
||||
frappe.patches.v13_0.set_unique_for_page_view
|
||||
frappe.patches.v13_0.remove_tailwind_from_page_builder
|
||||
frappe.patches.v13_0.rename_onboarding
|
||||
frappe.patches.v13_0.email_unsubscribe
|
||||
execute:frappe.delete_doc("Web Template", "Section with Left Image", force=1)
|
||||
execute:frappe.delete_doc("DocType", "Onboarding Slide")
|
||||
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
|
||||
|
|
|
|||
6
frappe/patches/v13_0/set_unique_for_page_view.py
Normal file
6
frappe/patches/v13_0/set_unique_for_page_view.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('website', 'doctype', 'web_page_view', force=True)
|
||||
site_url = frappe.utils.get_site_url(frappe.local.site)
|
||||
frappe.db.sql("""UPDATE `tabWeb Page View` set is_unique=1 where referrer LIKE '%{0}%'""".format(site_url))
|
||||
10
frappe/patches/v13_0/site_wise_logging.py
Normal file
10
frappe/patches/v13_0/site_wise_logging.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import os
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
site = frappe.local.site
|
||||
|
||||
log_folder = os.path.join(site, 'logs')
|
||||
if not os.path.exists(log_folder):
|
||||
os.mkdir(log_folder)
|
||||
54
frappe/patches/v13_0/update_date_filters_in_user_settings.py
Normal file
54
frappe/patches/v13_0/update_date_filters_in_user_settings.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe, json
|
||||
from frappe.model.utils.user_settings import update_user_settings, sync_user_settings
|
||||
|
||||
def execute():
|
||||
users = frappe.db.sql("select distinct(user) from `__UserSettings`", as_dict=True)
|
||||
|
||||
for user in users:
|
||||
user_settings = frappe.db.sql('''
|
||||
select
|
||||
* from `__UserSettings`
|
||||
where
|
||||
user="{user}"
|
||||
'''.format(user = user.user), as_dict=True)
|
||||
|
||||
for setting in user_settings:
|
||||
data = frappe.parse_json(setting.get('data'))
|
||||
if data:
|
||||
for key in data:
|
||||
update_user_setting_filters(data, key, setting)
|
||||
|
||||
sync_user_settings()
|
||||
|
||||
|
||||
def update_user_setting_filters(data, key, user_setting):
|
||||
timespan_map = {
|
||||
'1 week': 'week',
|
||||
'1 month': 'month',
|
||||
'3 months': 'quarter',
|
||||
'6 months': '6 months',
|
||||
'1 year': 'year',
|
||||
}
|
||||
|
||||
period_map = {
|
||||
'Previous': 'last',
|
||||
'Next': 'next'
|
||||
}
|
||||
|
||||
if data.get(key):
|
||||
update = False
|
||||
if isinstance(data.get(key), dict):
|
||||
filters = data.get(key).get('filters')
|
||||
if filters and isinstance(filters, list):
|
||||
for f in filters:
|
||||
if f[2] == 'Next' or f[2] == 'Previous':
|
||||
update = True
|
||||
f[3] = period_map[f[2]] + ' ' + timespan_map[f[3]]
|
||||
f[2] = 'Timespan'
|
||||
|
||||
if update:
|
||||
data[key]['filters'] = filters
|
||||
update_user_settings(user_setting['doctype'], json.dumps(data), for_update=True)
|
||||
|
||||
|
||||
28
frappe/patches/v13_0/update_duration_options.py
Normal file
28
frappe/patches/v13_0/update_duration_options.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('core', 'doctype', 'DocField')
|
||||
|
||||
if frappe.db.has_column('DocField', 'show_days'):
|
||||
frappe.db.sql("""
|
||||
UPDATE
|
||||
tabDocField
|
||||
SET
|
||||
hide_days = 1 WHERE show_days = 0
|
||||
""")
|
||||
frappe.db.sql_ddl('alter table tabDocField drop column show_days')
|
||||
|
||||
if frappe.db.has_column('DocField', 'show_seconds'):
|
||||
frappe.db.sql("""
|
||||
UPDATE
|
||||
tabDocField
|
||||
SET
|
||||
hide_seconds = 1 WHERE show_seconds = 0
|
||||
""")
|
||||
frappe.db.sql_ddl('alter table tabDocField drop column show_seconds')
|
||||
|
||||
frappe.clear_cache(doctype='DocField')
|
||||
|
|
@ -250,6 +250,8 @@
|
|||
"public/less/form_grid.less"
|
||||
],
|
||||
"js/form.min.js": [
|
||||
"public/js/frappe/form/templates/address_list.html",
|
||||
"public/js/frappe/form/templates/contact_list.html",
|
||||
"public/js/frappe/form/templates/print_layout.html",
|
||||
"public/js/frappe/form/templates/users_in_sidebar.html",
|
||||
"public/js/frappe/form/templates/set_sharing.html",
|
||||
|
|
|
|||
|
|
@ -1,82 +1,64 @@
|
|||
/* csslint ignore:start */
|
||||
|
||||
/* palette colors*/
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
color: #36414c;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid #d1d8dd;
|
||||
}
|
||||
|
||||
.body-table {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.body-table td {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.email-header,
|
||||
.email-body,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
border-top: 1px solid #d1d8dd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.email-header {
|
||||
border: 1px solid #d1d8dd;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.email-header .brand-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.email-header-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.body-table.has-header .email-body {
|
||||
border: 1px solid #d1d8dd;
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.body-table.has-header .email-footer {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.email-footer-container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.email-footer-container > div:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.email-unsubscribe a {
|
||||
color: #8d99a6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
padding: 7px 10px;
|
||||
|
|
@ -84,24 +66,20 @@ hr {
|
|||
border: 1px solid;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.btn.btn-default {
|
||||
color: #fff;
|
||||
background-color: #f0f4f7;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #5e64ff;
|
||||
border-color: #444bff;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table td,
|
||||
.table th {
|
||||
padding: 8px;
|
||||
|
|
@ -110,68 +88,53 @@ hr {
|
|||
border-top: 1px solid #d1d8dd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table > thead > tr > th {
|
||||
vertical-align: middle;
|
||||
border-bottom: 2px solid #d1d8dd;
|
||||
}
|
||||
|
||||
.table > thead:first-child > tr:first-child > th {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.table.table-bordered {
|
||||
border: 1px solid #d1d8dd;
|
||||
}
|
||||
|
||||
.table.table-bordered td,
|
||||
.table.table-bordered th {
|
||||
border: 1px solid #d1d8dd;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
font-size: 80% !important;
|
||||
color: #8d99a6 !important;
|
||||
border-top: 1px solid #ebeff2;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #8d99a6 !important;
|
||||
}
|
||||
|
||||
.text-extra-muted {
|
||||
color: #d1d8dd !important;
|
||||
}
|
||||
|
||||
.text-regular {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-medium {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
|
@ -180,43 +143,33 @@ hr {
|
|||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.indicator.indicator-blue {
|
||||
background-color: #5e64ff;
|
||||
}
|
||||
|
||||
.indicator.indicator-green {
|
||||
background-color: #98d85b;
|
||||
}
|
||||
|
||||
.indicator.indicator-orange {
|
||||
background-color: #ffa00a;
|
||||
}
|
||||
|
||||
.indicator.indicator-red {
|
||||
background-color: #ff5858;
|
||||
}
|
||||
|
||||
.indicator.indicator-yellow {
|
||||
background-color: #feef72;
|
||||
}
|
||||
|
||||
.screenshot {
|
||||
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #d1d8dd;
|
||||
margin: 8px 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.list-unstyled {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* auto email report */
|
||||
|
||||
.report-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* csslint ignore:end */
|
||||
|
|
|
|||
183
frappe/public/css/hljs-night-owl.css
Normal file
183
frappe/public/css/hljs-night-owl.css
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
|
||||
Night Owl for highlight.js (c) Carl Baxter <carl@cbax.tech>
|
||||
|
||||
An adaptation of Sarah Drasner's Night Owl VS Code Theme
|
||||
https://github.com/sdras/night-owl-vscode-theme
|
||||
|
||||
Copyright (c) 2018 Sarah Drasner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #011627;
|
||||
color: #d6deeb;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* General Purpose */
|
||||
.hljs-keyword {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-built_in {
|
||||
color: #addb67;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-type {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-literal {
|
||||
color: #ff5874;
|
||||
}
|
||||
.hljs-number {
|
||||
color: #F78C6C;
|
||||
}
|
||||
.hljs-regexp {
|
||||
color: #5ca7e4;
|
||||
}
|
||||
.hljs-string {
|
||||
color: #ecc48d;
|
||||
}
|
||||
.hljs-subst {
|
||||
color: #d3423e;
|
||||
}
|
||||
.hljs-symbol {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-class {
|
||||
color: #ffcb8b;
|
||||
}
|
||||
.hljs-function {
|
||||
color: #82AAFF;
|
||||
}
|
||||
.hljs-title {
|
||||
color: #DCDCAA;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-params {
|
||||
color: #7fdbca;
|
||||
}
|
||||
|
||||
/* Meta */
|
||||
.hljs-comment {
|
||||
color: #637777;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-doctag {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-meta {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-meta-keyword {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-meta-string {
|
||||
color: #ecc48d;
|
||||
}
|
||||
|
||||
/* Tags, attributes, config */
|
||||
.hljs-section {
|
||||
color: #82b1ff;
|
||||
}
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-builtin-name {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-attr {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-attribute {
|
||||
color: #80cbc4;
|
||||
}
|
||||
.hljs-variable {
|
||||
color: #addb67;
|
||||
}
|
||||
|
||||
/* Markup */
|
||||
.hljs-bullet {
|
||||
color: #d9f5dd;
|
||||
}
|
||||
.hljs-code {
|
||||
color: #80CBC4;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
color: #addb67;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-formula {
|
||||
color: #c792ea;
|
||||
}
|
||||
.hljs-link {
|
||||
color: #ff869a;
|
||||
}
|
||||
.hljs-quote {
|
||||
color: #697098;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* CSS */
|
||||
.hljs-selector-tag {
|
||||
color: #ff6363;
|
||||
}
|
||||
|
||||
.hljs-selector-id {
|
||||
color: #fad430;
|
||||
}
|
||||
|
||||
.hljs-selector-class {
|
||||
color: #addb67;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Templates */
|
||||
.hljs-template-tag {
|
||||
color: #c792ea;
|
||||
}
|
||||
.hljs-template-variable {
|
||||
color: #addb67;
|
||||
}
|
||||
|
||||
/* diff */
|
||||
.hljs-addition {
|
||||
color: #addb67ff;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #EF535090;
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ import './table_multiselect';
|
|||
import './multiselect_pills';
|
||||
import './multiselect_list';
|
||||
import './rating';
|
||||
import './duration';
|
||||
|
||||
frappe.ui.form.make_control = function (opts) {
|
||||
var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, "");
|
||||
|
|
|
|||
152
frappe/public/js/frappe/form/controls/duration.js
Normal file
152
frappe/public/js/frappe/form/controls/duration.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({
|
||||
make_input: function() {
|
||||
this._super();
|
||||
this.make_picker();
|
||||
},
|
||||
|
||||
make_picker: function() {
|
||||
this.inputs = [];
|
||||
this.set_duration_options();
|
||||
this.$picker = $(
|
||||
`<div class="duration-picker">
|
||||
<div class="picker-row row"></div>
|
||||
</div>`
|
||||
);
|
||||
this.$wrapper.append(this.$picker);
|
||||
this.build_numeric_input("days", this.duration_options.hide_days);
|
||||
this.build_numeric_input("hours", false);
|
||||
this.build_numeric_input("minutes", false);
|
||||
this.build_numeric_input("seconds", this.duration_options.hide_seconds);
|
||||
this.set_duration_picker_value(this.value);
|
||||
this.$picker.hide();
|
||||
this.bind_events();
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
build_numeric_input: function(label, hidden, max) {
|
||||
let $duration_input = $(`
|
||||
<input class="input-sm duration-input" data-duration="${label}" type="number" min="0" value="0">
|
||||
`);
|
||||
|
||||
let $input = $(`<div class="row duration-row"></div>`).prepend($duration_input);
|
||||
|
||||
if (max) {
|
||||
$duration_input.attr("max", max);
|
||||
}
|
||||
|
||||
this.inputs[label] = $duration_input;
|
||||
|
||||
let $control = $(`
|
||||
<div class="col duration-col">
|
||||
<div class="row duration-row duration-label">${__(label)}</div>
|
||||
</div>`
|
||||
);
|
||||
|
||||
if (hidden) {
|
||||
$control.addClass("hidden");
|
||||
}
|
||||
$control.prepend($input);
|
||||
$control.appendTo(this.$picker.find(".picker-row"));
|
||||
},
|
||||
|
||||
set_duration_options() {
|
||||
this.duration_options = frappe.utils.get_duration_options(this.df);
|
||||
},
|
||||
|
||||
set_duration_picker_value: function(value) {
|
||||
let total_duration = frappe.utils.seconds_to_duration(value, this.duration_options);
|
||||
|
||||
if (this.$picker) {
|
||||
Object.keys(total_duration).forEach(duration => {
|
||||
this.inputs[duration].prop("value", total_duration[duration]);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
bind_events: function() {
|
||||
// flag to handle the display property of the picker
|
||||
let clicked = false;
|
||||
|
||||
this.$wrapper.find(".duration-input").mousedown(() => {
|
||||
// input in individual duration boxes
|
||||
clicked = true;
|
||||
});
|
||||
|
||||
this.$picker.on("change", ".duration-input", () => {
|
||||
// duration changed in individual boxes
|
||||
clicked = false;
|
||||
let duration = this.get_duration();
|
||||
let value = frappe.utils.duration_to_seconds(
|
||||
duration.days,
|
||||
duration.hours,
|
||||
duration.minutes,
|
||||
duration.seconds
|
||||
);
|
||||
this.set_value(value);
|
||||
this.set_focus();
|
||||
});
|
||||
|
||||
this.$input.on("focus", () => {
|
||||
this.$picker.show();
|
||||
let is_picker_set = this.is_duration_picker_set(this.inputs);
|
||||
if (!is_picker_set) {
|
||||
this.set_duration_picker_value(this.value);
|
||||
}
|
||||
});
|
||||
|
||||
this.$input.on("blur", () => {
|
||||
// input in duration boxes, don't close the picker
|
||||
if (clicked) {
|
||||
clicked = false;
|
||||
} else {
|
||||
// blur event was not due to duration inputs
|
||||
this.$picker.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
get_value() {
|
||||
return cint(this.value);
|
||||
},
|
||||
|
||||
refresh_input: function() {
|
||||
this._super();
|
||||
this.set_duration_options();
|
||||
this.set_duration_picker_value(this.value);
|
||||
},
|
||||
|
||||
format_for_input: function(value) {
|
||||
return frappe.utils.get_formatted_duration(value, this.duration_options);
|
||||
},
|
||||
|
||||
get_duration() {
|
||||
// returns an object of days, hours, minutes and seconds from the inputs array
|
||||
let total_duration = {
|
||||
minutes: 0,
|
||||
hours: 0,
|
||||
days: 0,
|
||||
seconds: 0
|
||||
};
|
||||
if (this.inputs) {
|
||||
total_duration.minutes = parseInt(this.inputs.minutes.val());
|
||||
total_duration.hours = parseInt(this.inputs.hours.val());
|
||||
if (!this.duration_options.hide_days) {
|
||||
total_duration.days = parseInt(this.inputs.days.val());
|
||||
}
|
||||
if (!this.duration_options.hide_seconds) {
|
||||
total_duration.seconds = parseInt(this.inputs.seconds.val());
|
||||
}
|
||||
}
|
||||
return total_duration;
|
||||
},
|
||||
|
||||
is_duration_picker_set(inputs) {
|
||||
let is_set = false;
|
||||
Object.values(inputs).forEach(duration => {
|
||||
if (duration.prop("value") != 0) {
|
||||
is_set = true;
|
||||
}
|
||||
});
|
||||
return is_set;
|
||||
}
|
||||
});
|
||||
|
|
@ -129,7 +129,8 @@ frappe.ui.form.ControlMultiSelectPills = frappe.ui.form.ControlAutocomplete.exte
|
|||
get_data() {
|
||||
let data;
|
||||
if(this.df.get_data) {
|
||||
data = this.df.get_data();
|
||||
let txt = this.$input.val();
|
||||
data = this.df.get_data(txt);
|
||||
if (data && data.then) {
|
||||
data.then((r) => {
|
||||
this.set_data(r);
|
||||
|
|
|
|||
|
|
@ -589,7 +589,6 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
out.push(me.get_version_comment(version, message));
|
||||
}
|
||||
} else {
|
||||
p = p.map(frappe.utils.escape_html);
|
||||
const df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname);
|
||||
if (df && !df.hidden) {
|
||||
const field_display_status = frappe.perm.get_field_display_status(df, null,
|
||||
|
|
@ -597,8 +596,8 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
if (field_display_status === 'Read' || field_display_status === 'Write') {
|
||||
parts.push(__('{0} from {1} to {2}', [
|
||||
__(df.label),
|
||||
(frappe.ellipsis(frappe.utils.html2text(p[1]), 40) || '""').bold(),
|
||||
(frappe.ellipsis(frappe.utils.html2text(p[2]), 40) || '""').bold()
|
||||
me.format_content_for_timeline(p[1]),
|
||||
me.format_content_for_timeline(p[2])
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
@ -608,9 +607,9 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
if (parts.length) {
|
||||
let message;
|
||||
if (updater_reference_link) {
|
||||
message = __("changed value of {0} {1}", [parts.join(', ').bold(), updater_reference_link]);
|
||||
message = __("changed value of {0} {1}", [parts.join(', '), updater_reference_link]);
|
||||
} else {
|
||||
message = __("changed value of {0}", [parts.join(', ').bold()]);
|
||||
message = __("changed value of {0}", [parts.join(', ')]);
|
||||
}
|
||||
out.push(me.get_version_comment(version, message));
|
||||
}
|
||||
|
|
@ -618,23 +617,23 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
|
||||
// value changed in table field
|
||||
if (data.row_changed && data.row_changed.length) {
|
||||
var parts = [], count = 0;
|
||||
let parts = [];
|
||||
data.row_changed.every(function(row) {
|
||||
row[3].every(function(p) {
|
||||
var df = me.frm.fields_dict[row[0]] &&
|
||||
frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype,
|
||||
p[0], me.frm.docname);
|
||||
|
||||
if(df && !df.hidden) {
|
||||
if (df && !df.hidden) {
|
||||
var field_display_status = frappe.perm.get_field_display_status(df,
|
||||
null, me.frm.perm);
|
||||
|
||||
if(field_display_status === 'Read' || field_display_status === 'Write') {
|
||||
if (field_display_status === 'Read' || field_display_status === 'Write') {
|
||||
parts.push(__('{0} from {1} to {2} in row #{3}', [
|
||||
frappe.meta.get_label(me.frm.fields_dict[row[0]].grid.doctype,
|
||||
p[0]),
|
||||
(frappe.ellipsis(p[1], 40) || '""').bold(),
|
||||
(frappe.ellipsis(p[2], 40) || '""').bold(),
|
||||
me.format_content_for_timeline(p[1]),
|
||||
me.format_content_for_timeline(p[2]),
|
||||
row[1]
|
||||
]));
|
||||
}
|
||||
|
|
@ -657,20 +656,22 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
// rows added / removed
|
||||
// __('added'), __('removed') # for translation, don't remove
|
||||
['added', 'removed'].forEach(function(key) {
|
||||
if(data[key] && data[key].length) {
|
||||
parts = (data[key] || []).map(function(p) {
|
||||
if (data[key] && data[key].length) {
|
||||
let parts = (data[key] || []).map(function(p) {
|
||||
var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname);
|
||||
if(df && !df.hidden) {
|
||||
if (df && !df.hidden) {
|
||||
var field_display_status = frappe.perm.get_field_display_status(df, null,
|
||||
me.frm.perm);
|
||||
|
||||
if(field_display_status === 'Read' || field_display_status === 'Write') {
|
||||
if (field_display_status === 'Read' || field_display_status === 'Write') {
|
||||
return frappe.meta.get_label(me.frm.doctype, p[0])
|
||||
}
|
||||
}
|
||||
});
|
||||
parts = parts.filter(function(p) { return p; });
|
||||
if(parts.length) {
|
||||
parts = parts.filter(function(p) {
|
||||
return p;
|
||||
});
|
||||
if (parts.length) {
|
||||
out.push(me.get_version_comment(version, __("{0} rows for {1}",
|
||||
[__(key), parts.join(', ')])));
|
||||
}
|
||||
|
|
@ -717,6 +718,17 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
|
||||
}
|
||||
|
||||
format_content_for_timeline(content) {
|
||||
// text to HTML
|
||||
// limits content to 40 characters
|
||||
// escapes HTML
|
||||
// and makes it bold
|
||||
content = frappe.utils.html2text(content);
|
||||
content = frappe.ellipsis(content, 40) || '""';
|
||||
content = frappe.utils.escape_html(content);
|
||||
return content.bold();
|
||||
}
|
||||
|
||||
delete_comment(name) {
|
||||
var me = this;
|
||||
|
||||
|
|
|
|||
|
|
@ -650,13 +650,14 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
frappe.utils.play_sound("submit");
|
||||
callback && callback();
|
||||
me.script_manager.trigger("on_submit")
|
||||
.then(() => resolve(me));
|
||||
if (frappe.route_hooks.after_submit) {
|
||||
let route_callback = frappe.route_hooks.after_submit;
|
||||
delete frappe.route_hooks.after_submit;
|
||||
|
||||
route_callback(me);
|
||||
}
|
||||
.then(() => resolve(me))
|
||||
.then(() => {
|
||||
if (frappe.route_hooks.after_submit) {
|
||||
let route_callback = frappe.route_hooks.after_submit;
|
||||
delete frappe.route_hooks.after_submit;
|
||||
route_callback(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, btn, () => me.handle_save_fail(btn, on_error), resolve);
|
||||
});
|
||||
|
|
@ -786,15 +787,24 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
frappe.msgprint(__('"amended_from" field must be present to do an amendment.'));
|
||||
return;
|
||||
}
|
||||
this.validate_form_action("Amend");
|
||||
var me = this;
|
||||
var fn = function(newdoc) {
|
||||
newdoc.amended_from = me.docname;
|
||||
if(me.fields_dict && me.fields_dict['amendment_date'])
|
||||
newdoc.amendment_date = frappe.datetime.obj_to_str(new Date());
|
||||
};
|
||||
this.copy_doc(fn, 1);
|
||||
frappe.utils.play_sound("click");
|
||||
|
||||
frappe.xcall('frappe.client.is_document_amended', {
|
||||
'doctype': this.doc.doctype,
|
||||
'docname': this.doc.name
|
||||
}).then(is_amended => {
|
||||
if (is_amended) {
|
||||
frappe.throw(__('This document is already amended, you cannot ammend it again'));
|
||||
}
|
||||
this.validate_form_action("Amend");
|
||||
var me = this;
|
||||
var fn = function(newdoc) {
|
||||
newdoc.amended_from = me.docname;
|
||||
if (me.fields_dict && me.fields_dict['amendment_date'])
|
||||
newdoc.amendment_date = frappe.datetime.obj_to_str(new Date());
|
||||
};
|
||||
this.copy_doc(fn, 1);
|
||||
frappe.utils.play_sound("click");
|
||||
});
|
||||
}
|
||||
|
||||
validate_form_action(action, resolve) {
|
||||
|
|
@ -1586,7 +1596,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
let steps = frappe.tour[this.doctype].map(step => {
|
||||
let field = this.get_docfield(step.fieldname);
|
||||
return {
|
||||
element: `.frappe-control[title='${step.fieldname}']`,
|
||||
element: `.frappe-control[data-fieldname='${step.fieldname}']`,
|
||||
popover: {
|
||||
title: step.title || field.label,
|
||||
description: step.description
|
||||
|
|
|
|||
|
|
@ -142,10 +142,7 @@ frappe.form.formatters = {
|
|||
},
|
||||
DateRange: function(value) {
|
||||
if($.isArray(value)) {
|
||||
return __("{0} to {1}", [
|
||||
frappe.datetime.str_to_user(value[0]),
|
||||
frappe.datetime.str_to_user(value[1])
|
||||
]);
|
||||
return __("{0} to {1}", [frappe.datetime.str_to_user(value[0]), frappe.datetime.str_to_user(value[1])]);
|
||||
} else {
|
||||
return value || "";
|
||||
}
|
||||
|
|
@ -188,6 +185,14 @@ frappe.form.formatters = {
|
|||
|
||||
return value || "";
|
||||
},
|
||||
Duration: function(value, docfield) {
|
||||
if (value) {
|
||||
let duration_options = frappe.utils.get_duration_options(docfield);
|
||||
value = frappe.utils.get_formatted_duration(value, duration_options);
|
||||
}
|
||||
|
||||
return value || "";
|
||||
},
|
||||
LikedBy: function(value) {
|
||||
var html = "";
|
||||
$.each(JSON.parse(value || "[]"), function(i, v) {
|
||||
|
|
|
|||
|
|
@ -1,110 +1,62 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.ui.form.MultiSelectDialog = Class.extend({
|
||||
init: function(opts) {
|
||||
/* Options: doctype, target, setters, get_query, action */
|
||||
$.extend(this, opts);
|
||||
|
||||
frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
||||
constructor(opts) {
|
||||
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
|
||||
Object.assign(this, opts);
|
||||
var me = this;
|
||||
if(this.doctype!="[Select]") {
|
||||
frappe.model.with_doctype(this.doctype, function(r) {
|
||||
if (this.doctype != "[Select]") {
|
||||
frappe.model.with_doctype(this.doctype, function () {
|
||||
me.make();
|
||||
});
|
||||
} else {
|
||||
this.make();
|
||||
}
|
||||
},
|
||||
make: function() {
|
||||
let me = this;
|
||||
}
|
||||
|
||||
make() {
|
||||
let me = this;
|
||||
this.page_length = 20;
|
||||
this.start = 0;
|
||||
let fields = this.get_primary_filters();
|
||||
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Search Term"),
|
||||
fieldname: "search_term"
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break"
|
||||
}
|
||||
];
|
||||
let count = 0;
|
||||
if(!this.date_field) {
|
||||
this.date_field = "transaction_date";
|
||||
}
|
||||
|
||||
// setters can be defined as a dict or a list of fields
|
||||
// setters define the additional filters that get applied
|
||||
// for selection
|
||||
|
||||
// CASE 1: DocType name and fieldname is the same, example "customer" and "customer"
|
||||
// setters define the filters applied in the modal
|
||||
// if the fieldnames and doctypes are consistently named,
|
||||
// pass a dict with the setter key and value, for example
|
||||
// {customer: [customer_name]}
|
||||
|
||||
// CASE 2: if the fieldname of the target is different,
|
||||
// then pass a list of fields with appropriate fieldname
|
||||
|
||||
if($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
fields.push(df, {fieldtype: "Column Break"});
|
||||
}
|
||||
} else {
|
||||
Object.keys(this.setters).forEach(function(setter) {
|
||||
fields.push({
|
||||
fieldtype: me.target.fields_dict[setter].df.fieldtype,
|
||||
label: me.target.fields_dict[setter].df.label,
|
||||
fieldname: setter,
|
||||
options: me.target.fields_dict[setter].df.options,
|
||||
default: me.setters[setter]
|
||||
});
|
||||
if (count++ < Object.keys(me.setters).length) {
|
||||
fields.push({fieldtype: "Column Break"});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Make results area
|
||||
fields = fields.concat([
|
||||
{
|
||||
"fieldname":"date_range",
|
||||
"label": __("Date Range"),
|
||||
"fieldtype": "DateRange",
|
||||
},
|
||||
{ fieldtype: "Section Break" },
|
||||
{ fieldtype: "HTML", fieldname: "results_area" },
|
||||
{ fieldtype: "Button", fieldname: "more_btn", label: __("More"),
|
||||
click: function(){
|
||||
me.start += 20;
|
||||
frappe.flags.auto_scroll = true;
|
||||
me.get_results();
|
||||
{
|
||||
fieldtype: "Button", fieldname: "more_btn", label: __("More"),
|
||||
click: () => {
|
||||
this.start += 20;
|
||||
this.get_results();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's'
|
||||
: this.doctype.slice(0, -1) + 'ies';
|
||||
// Custom Data Fields
|
||||
if (this.data_fields) {
|
||||
fields.push({ fieldtype: "Section Break" });
|
||||
fields = fields.concat(this.data_fields);
|
||||
}
|
||||
|
||||
let doctype_plural = this.doctype.plural();
|
||||
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]),
|
||||
title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]),
|
||||
fields: fields,
|
||||
primary_action_label: __("Get Items"),
|
||||
primary_action_label: this.primary_action_label || __("Get Items"),
|
||||
secondary_action_label: __("Make {0}", [me.doctype]),
|
||||
primary_action: function() {
|
||||
me.action(me.get_checked_values(), me.args);
|
||||
primary_action: function () {
|
||||
let filters_data = me.get_custom_filters();
|
||||
me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data);
|
||||
},
|
||||
secondary_action: function(e) {
|
||||
secondary_action: function (e) {
|
||||
// If user wants to close the modal
|
||||
if (e) {
|
||||
frappe.route_options = {};
|
||||
if($.isArray(me.setters)) {
|
||||
if (Array.isArray(me.setters)) {
|
||||
for (let df of me.setters) {
|
||||
frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
|
||||
}
|
||||
} else {
|
||||
Object.keys(me.setters).forEach(function(setter) {
|
||||
Object.keys(me.setters).forEach(function (setter) {
|
||||
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
|
||||
});
|
||||
}
|
||||
|
|
@ -114,6 +66,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
}
|
||||
});
|
||||
|
||||
if (this.add_filters_group) {
|
||||
this.make_filter_area();
|
||||
}
|
||||
|
||||
this.$parent = $(this.dialog.body);
|
||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results"
|
||||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);
|
||||
|
|
@ -126,9 +82,89 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
this.bind_events();
|
||||
this.get_results();
|
||||
this.dialog.show();
|
||||
},
|
||||
}
|
||||
|
||||
bind_events: function() {
|
||||
get_primary_filters() {
|
||||
let fields = [];
|
||||
|
||||
let columns = new Array(3);
|
||||
|
||||
// Hack for three column layout
|
||||
// To add column break
|
||||
columns[0] = [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Search"),
|
||||
fieldname: "search_term"
|
||||
}
|
||||
];
|
||||
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]
|
||||
});
|
||||
});
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal
|
||||
if (Object.seal) {
|
||||
Object.seal(columns);
|
||||
// now a is a fixed-size array with mutable entries
|
||||
}
|
||||
|
||||
fields = [
|
||||
...columns[0],
|
||||
{ fieldtype: "Column Break" },
|
||||
...columns[1],
|
||||
{ fieldtype: "Column Break" },
|
||||
...columns[2],
|
||||
{ fieldtype: "Section Break", fieldname: "primary_filters_sb" }
|
||||
];
|
||||
|
||||
if (this.add_filters_group) {
|
||||
fields.push(
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'filter_area',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
make_filter_area() {
|
||||
this.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: this.dialog.get_field('filter_area').$wrapper,
|
||||
doctype: this.doctype,
|
||||
on_change: () => {
|
||||
this.get_results();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_custom_filters() {
|
||||
if (this.add_filters_group && this.filter_group) {
|
||||
return this.filter_group.get_filters().reduce((acc, filter) => {
|
||||
return Object.assign(acc, {
|
||||
[filter[1]]: [filter[2], filter[3]]
|
||||
});
|
||||
}, {});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
let me = this;
|
||||
|
||||
this.$results.on('click', '.list-item-container', function (e) {
|
||||
|
|
@ -136,48 +172,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
$(this).find(':checkbox').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
this.$results.on('click', '.list-item--head :checkbox', (e) => {
|
||||
this.$results.find('.list-item-container .list-row-check')
|
||||
.prop("checked", ($(e.target).is(':checked')));
|
||||
});
|
||||
|
||||
this.$parent.find('.input-with-feedback').on('change', (e) => {
|
||||
this.$parent.find('.input-with-feedback').on('change', () => {
|
||||
frappe.flags.auto_scroll = false;
|
||||
this.get_results();
|
||||
});
|
||||
|
||||
this.$parent.find('[data-fieldname="date_range"]').on('blur', (e) => {
|
||||
frappe.flags.auto_scroll = false;
|
||||
this.get_results();
|
||||
});
|
||||
|
||||
this.$parent.find('[data-fieldname="search_term"]').on('input', (e) => {
|
||||
this.$parent.find('[data-fieldtype="Data"]').on('input', () => {
|
||||
var $this = $(this);
|
||||
clearTimeout($this.data('timeout'));
|
||||
$this.data('timeout', setTimeout(function() {
|
||||
$this.data('timeout', setTimeout(function () {
|
||||
frappe.flags.auto_scroll = false;
|
||||
me.empty_list();
|
||||
me.get_results();
|
||||
}, 300));
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
get_checked_values: function() {
|
||||
get_checked_values() {
|
||||
// Return name of checked value.
|
||||
return this.$results.find('.list-item-container').map(function() {
|
||||
if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) {
|
||||
return this.$results.find('.list-item-container').map(function () {
|
||||
if ($(this).find('.list-row-check:checkbox:checked').length > 0) {
|
||||
return $(this).attr('data-item-name');
|
||||
}
|
||||
}).get();
|
||||
},
|
||||
}
|
||||
|
||||
get_checked_items: function() {
|
||||
get_checked_items() {
|
||||
// Return checked items with all the column values.
|
||||
let checked_values = this.get_checked_values();
|
||||
return this.results.filter(res => checked_values.includes(res.name));
|
||||
},
|
||||
}
|
||||
|
||||
make_list_row: function(result={}) {
|
||||
make_list_row(result = {}) {
|
||||
var me = this;
|
||||
// Make a head row by default (if result not passed)
|
||||
let head = Object.keys(result).length === 0;
|
||||
|
|
@ -185,26 +217,17 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
let contents = ``;
|
||||
let columns = ["name"];
|
||||
|
||||
if($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
columns.push(df.fieldname);
|
||||
}
|
||||
} else {
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
}
|
||||
columns.push("Date");
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
|
||||
columns.forEach(function(column) {
|
||||
columns.forEach(function (column) {
|
||||
contents += `<div class="list-item__content ellipsis">
|
||||
${
|
||||
head ? `<span class="ellipsis">${__(frappe.model.unscrub(column))}</span>`
|
||||
|
||||
: (column !== "name" ? `<span class="ellipsis">${__(result[column])}</span>`
|
||||
: `<a href="${"#Form/"+ me.doctype + "/" + result[column]}" class="list-id ellipsis">
|
||||
${__(result[column])}</a>`)
|
||||
}
|
||||
head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>`
|
||||
: (column !== "name" ? `<span class="ellipsis result-row" title="${__(result[column] || '')}">${__(result[column] || '')}</span>`
|
||||
: `<a href="${"#Form/" + me.doctype + "/" + result[column] || ''}" class="list-id ellipsis" title="${__(result[column] || '')}">
|
||||
${__(result[column] || '')}</a>`)}
|
||||
</div>`;
|
||||
})
|
||||
});
|
||||
|
||||
let $row = $(`<div class="list-item">
|
||||
<div class="list-item__content" style="flex: 0 0 10px;">
|
||||
|
|
@ -215,10 +238,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
|
||||
head ? $row.addClass('list-item--head')
|
||||
: $row = $(`<div class="list-item-container" data-item-name="${result.name}"></div>`).append($row);
|
||||
return $row;
|
||||
},
|
||||
|
||||
render_result_list: function(results, more = 0, empty=true) {
|
||||
$(".modal-dialog .list-item--head").css("z-index", 0);
|
||||
return $row;
|
||||
}
|
||||
|
||||
render_result_list(results, more = 0, empty = true) {
|
||||
var me = this;
|
||||
var more_btn = me.dialog.fields_dict.more_btn.$wrapper;
|
||||
|
||||
|
|
@ -240,44 +265,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
});
|
||||
|
||||
if (frappe.flags.auto_scroll) {
|
||||
this.$results.animate({scrollTop: me.$results.prop('scrollHeight')}, 500);
|
||||
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
empty_list: function() {
|
||||
empty_list() {
|
||||
// Store all checked items
|
||||
let checked = this.get_checked_items().map(item => {
|
||||
return {
|
||||
...item,
|
||||
checked: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Remove **all** items
|
||||
this.$results.find('.list-item-container').remove();
|
||||
|
||||
// Rerender checked items
|
||||
this.render_result_list(checked, 0, false);
|
||||
},
|
||||
}
|
||||
|
||||
get_results: function() {
|
||||
get_results() {
|
||||
let me = this;
|
||||
let filters = this.get_query ? this.get_query().filters : {} || {};
|
||||
let filter_fields = [];
|
||||
|
||||
let filters = this.get_query ? this.get_query().filters : {};
|
||||
let filter_fields = [me.date_field];
|
||||
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) {
|
||||
filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
|
||||
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 date_val = this.dialog.fields_dict["date_range"].get_value();
|
||||
if(date_val) {
|
||||
filters[this.date_field] = ['between', date_val];
|
||||
}
|
||||
let filter_group = this.get_custom_filters();
|
||||
Object.assign(filters, filter_group);
|
||||
|
||||
let args = {
|
||||
doctype: me.doctype,
|
||||
|
|
@ -288,13 +313,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
page_length: this.page_length + 1,
|
||||
query: this.get_query ? this.get_query().query : '',
|
||||
as_dict: 1
|
||||
}
|
||||
};
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method:'frappe.desk.search.search_widget',
|
||||
method: 'frappe.desk.search.search_widget',
|
||||
no_spinner: true,
|
||||
args: args,
|
||||
callback: function(r) {
|
||||
callback: function (r) {
|
||||
let more = 0;
|
||||
me.results = [];
|
||||
if (r.values.length) {
|
||||
|
|
@ -302,30 +327,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
r.values.pop();
|
||||
more = 1;
|
||||
}
|
||||
r.values.forEach(function(result) {
|
||||
if(me.date_field in result) {
|
||||
result["Date"] = result[me.date_field]
|
||||
}
|
||||
r.values.forEach(function (result) {
|
||||
result.checked = 0;
|
||||
result.parsed_date = Date.parse(result["Date"]);
|
||||
me.results.push(result);
|
||||
});
|
||||
me.results.map( (result) => {
|
||||
result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"});
|
||||
})
|
||||
|
||||
me.results.sort((a, b) => {
|
||||
return a.parsed_date - b.parsed_date;
|
||||
});
|
||||
|
||||
// Preselect oldest entry
|
||||
if (me.start < 1 && r.values.length === 1) {
|
||||
me.results[0].checked = 1;
|
||||
}
|
||||
}
|
||||
me.render_result_list(me.results, more);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -16,12 +16,22 @@ frappe.ui.form.get_event_handler_list = function(doctype, fieldname) {
|
|||
frappe.ui.form.on = frappe.ui.form.on_change = function(doctype, fieldname, handler) {
|
||||
var add_handler = function(fieldname, handler) {
|
||||
var handler_list = frappe.ui.form.get_event_handler_list(doctype, fieldname);
|
||||
handler_list.push(handler);
|
||||
|
||||
let _handler = (...args) => {
|
||||
try {
|
||||
handler(...args);
|
||||
} catch (error) {
|
||||
console.error(handler);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
handler_list.push(_handler);
|
||||
|
||||
// add last handler to events so it can be called as
|
||||
// frm.events.handler(frm)
|
||||
if(cur_frm && cur_frm.doctype===doctype) {
|
||||
cur_frm.events[fieldname] = handler;
|
||||
cur_frm.events[fieldname] = _handler;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
22
frappe/public/js/frappe/form/templates/address_list.html
Normal file
22
frappe/public/js/frappe/form/templates/address_list.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<div class="clearfix"></div>
|
||||
{% for(var i=0, l=addr_list.length; i<l; i++) { %}
|
||||
<div class="address-box">
|
||||
<p class="h6">
|
||||
{%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
|
||||
<span class="text-muted">({%= __(addr_list[i].address_type) %})</span>{% } %}
|
||||
{% if(addr_list[i].is_primary_address) { %}
|
||||
<span class="text-muted">({%= __("Primary") %})</span>{% } %}
|
||||
{% if(addr_list[i].is_shipping_address) { %}
|
||||
<span class="text-muted">({%= __("Shipping") %})</span>{% } %}
|
||||
|
||||
<a href="#Form/Address/{%= encodeURIComponent(addr_list[i].name) %}" class="btn btn-default btn-xs pull-right"
|
||||
style="margin-top:-3px; margin-right: -5px;">
|
||||
{%= __("Edit") %}</a>
|
||||
</p>
|
||||
<p>{%= addr_list[i].display %}</p>
|
||||
</div>
|
||||
{% } %}
|
||||
{% if(!addr_list.length) { %}
|
||||
<p class="text-muted small">{%= __("No address added yet.") %}</p>
|
||||
{% } %}
|
||||
<p><button class="btn btn-xs btn-default btn-address">{{ __("New Address") }}</button></p>
|
||||
54
frappe/public/js/frappe/form/templates/contact_list.html
Normal file
54
frappe/public/js/frappe/form/templates/contact_list.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<div class="clearfix"></div>
|
||||
{% for(var i=0, l=contact_list.length; i<l; i++) { %}
|
||||
<div class="address-box">
|
||||
<p class="h6">
|
||||
{%= contact_list[i].first_name %} {%= contact_list[i].last_name %}
|
||||
{% if(contact_list[i].is_primary_contact) { %}
|
||||
<span class="text-muted">({%= __("Primary") %})</span>
|
||||
{% } %}
|
||||
{% if(contact_list[i].designation){ %}
|
||||
<span class="text-muted">– {%= contact_list[i].designation %}</span>
|
||||
{% } %}
|
||||
<a href="#Form/Contact/{%= encodeURIComponent(contact_list[i].name) %}"
|
||||
class="btn btn-xs btn-default pull-right"
|
||||
style="margin-top:-3px; margin-right: -5px;">
|
||||
{%= __("Edit") %}</a>
|
||||
</p>
|
||||
{% if (contact_list[i].phones || contact_list[i].email_ids) { %}
|
||||
<p>
|
||||
{% if(contact_list[i].phone) { %}
|
||||
{%= __("Phone") %}: {%= contact_list[i].phone %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].mobile_no) { %}
|
||||
{%= __("Mobile No") %}: {%= contact_list[i].mobile_no %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].phone_nos) { %}
|
||||
{% for(var j=0, k=contact_list[i].phone_nos.length; j<k; j++) { %}
|
||||
{%= __("Phone") %}: {%= contact_list[i].phone_nos[j].phone %}<br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if(contact_list[i].email_id) { %}
|
||||
{%= __("Email") %}: {%= contact_list[i].email_id %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].email_ids) { %}
|
||||
{% for(var j=0, k=contact_list[i].email_ids.length; j<k; j++) { %}
|
||||
{%= __("Email") %}: {%= contact_list[i].email_ids[j].email_id %}<br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
{% if (contact_list[i].address) { %}
|
||||
{%= __("Address") %}: {%= contact_list[i].address %}<br>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% } %}
|
||||
{% if(!contact_list.length) { %}
|
||||
<p class="text-muted small">{%= __("No contacts added yet.") %}</p>
|
||||
{% } %}
|
||||
<p><button class="btn btn-xs btn-default btn-contact">
|
||||
{{ __("New Contact") }}</button>
|
||||
</p>
|
||||
|
|
@ -374,19 +374,24 @@ frappe.ui.form.Toolbar = Class.extend({
|
|||
|
||||
var status = this.get_action_status();
|
||||
if (status) {
|
||||
if (status !== this.current_status) {
|
||||
if (status === 'Amend') {
|
||||
let doc = this.frm.doc;
|
||||
frappe.xcall('frappe.client.is_document_amended', {
|
||||
'doctype': doc.doctype,
|
||||
'docname': doc.name
|
||||
}).then(is_amended => {
|
||||
if (is_amended) return;
|
||||
this.set_page_actions(status);
|
||||
});
|
||||
} else {
|
||||
// When moving from a page with status amend to another page with status amend
|
||||
// We need to check if document is already amened specifcally and hide
|
||||
// or clear the menu actions accordingly
|
||||
|
||||
if (status !== this.current_status || status === 'Amend') {
|
||||
let doc = this.frm.doc;
|
||||
frappe.xcall('frappe.client.is_document_amended', {
|
||||
'doctype': doc.doctype,
|
||||
'docname': doc.name
|
||||
}).then(is_amended => {
|
||||
if (is_amended) {
|
||||
this.page.clear_actions();
|
||||
return;
|
||||
}
|
||||
this.set_page_actions(status);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.set_page_actions(status);
|
||||
}
|
||||
} else {
|
||||
this.page.clear_actions();
|
||||
|
|
|
|||
|
|
@ -338,6 +338,11 @@ frappe.views.BaseList = class BaseList {
|
|||
: [];
|
||||
}
|
||||
|
||||
get_filter_value(fieldname) {
|
||||
return this.get_filters_for_args().filter(f => f[1] == fieldname)[0] &&
|
||||
this.get_filters_for_args().filter(f => f[1] == fieldname)[0][3];
|
||||
}
|
||||
|
||||
get_args() {
|
||||
return {
|
||||
doctype: this.doctype,
|
||||
|
|
|
|||
383
frappe/public/js/frappe/list/list_settings.js
Normal file
383
frappe/public/js/frappe/list/list_settings.js
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
export default class ListSettings {
|
||||
constructor({ listview, doctype, meta, settings }) {
|
||||
if (!doctype) {
|
||||
frappe.throw(__('Doctype required'));
|
||||
}
|
||||
|
||||
this.listview = listview;
|
||||
this.doctype = doctype;
|
||||
this.meta = meta;
|
||||
this.settings = settings;
|
||||
this.dialog = null;
|
||||
this.fields = this.settings && this.settings.fields ? JSON.parse(this.settings.fields) : [];
|
||||
this.subject_field = null;
|
||||
|
||||
frappe.model.with_doctype("List View Settings", () => {
|
||||
this.make();
|
||||
this.get_listview_fields(meta);
|
||||
this.setup_fields();
|
||||
this.setup_remove_fields();
|
||||
this.add_new_fields();
|
||||
this.show_dialog();
|
||||
});
|
||||
}
|
||||
|
||||
make() {
|
||||
let me = this;
|
||||
|
||||
let list_view_settings = frappe.get_meta("List View Settings");
|
||||
|
||||
me.dialog = new frappe.ui.Dialog({
|
||||
title: __("{0} Settings", [__(me.doctype)]),
|
||||
fields: list_view_settings.fields
|
||||
});
|
||||
me.dialog.set_values(me.settings);
|
||||
me.dialog.set_primary_action(__('Save'), () => {
|
||||
let values = me.dialog.get_values();
|
||||
|
||||
frappe.show_alert({
|
||||
message: __("Saving"),
|
||||
indicator: "green"
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.list_view_settings.list_view_settings.save_listview_settings",
|
||||
args: {
|
||||
doctype: me.doctype,
|
||||
listview_settings: values,
|
||||
removed_listview_fields: me.removed_fields || []
|
||||
},
|
||||
callback: function (r) {
|
||||
me.listview.refresh_columns(r.message.meta, r.message.listview_settings);
|
||||
me.dialog.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
me.dialog.fields_dict["total_fields"].df.onchange = () => me.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
let me = this;
|
||||
|
||||
me.setup_fields();
|
||||
me.add_new_fields();
|
||||
me.setup_remove_fields();
|
||||
}
|
||||
|
||||
show_dialog() {
|
||||
let me = this;
|
||||
|
||||
if (!this.settings.fields) {
|
||||
me.update_fields();
|
||||
}
|
||||
|
||||
if (!me.dialog.get_value("total_fields")) {
|
||||
let field_count = me.fields.length;
|
||||
|
||||
if (field_count < 4) {
|
||||
field_count = 4;
|
||||
} else if (field_count > 10) {
|
||||
field_count = 4;
|
||||
}
|
||||
|
||||
me.dialog.set_value("total_fields", field_count);
|
||||
}
|
||||
|
||||
me.dialog.show();
|
||||
}
|
||||
|
||||
setup_fields() {
|
||||
function is_status_field(field) {
|
||||
return field.fieldname === "status_field";
|
||||
}
|
||||
|
||||
let me = this;
|
||||
|
||||
let fields_html = me.dialog.get_field("fields_html");
|
||||
let wrapper = fields_html.$wrapper[0];
|
||||
let fields = ``;
|
||||
let total_fields = me.dialog.get_values().total_fields || me.settings.total_fields;
|
||||
|
||||
for (let idx in me.fields) {
|
||||
if (idx == parseInt(total_fields)) {
|
||||
break;
|
||||
}
|
||||
let is_sortable = (idx == 0) ? `` : `sortable`;
|
||||
let show_sortable_handle = (idx == 0) ? `hide` : ``;
|
||||
let can_remove = (idx == 0 || is_status_field(me.fields[idx])) ? `hide` : ``;
|
||||
|
||||
fields += `
|
||||
<div class="control-input flex align-center form-control fields_order ${is_sortable}"
|
||||
style="display: block; margin-bottom: 5px;" data-fieldname="${me.fields[idx].fieldname}"
|
||||
data-label="${me.fields[idx].label}" data-type="${me.fields[idx].type}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-1">
|
||||
<i class="fa fa-bars text-muted sortable-handle ${show_sortable_handle}" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-md-10" style="padding-left:0px;">
|
||||
${me.fields[idx].label}
|
||||
</div>
|
||||
<div class="col-md-1 ${can_remove}">
|
||||
<a class="text-muted remove-field" data-fieldname="${me.fields[idx].fieldname}">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
fields_html.html(`
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label" style="padding-right: 0px;">Fields</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
${fields}
|
||||
</div>
|
||||
<p class="help-box small text-muted hidden-xs">
|
||||
<a class="add-new-fields text-muted">
|
||||
+ Add / Remove Fields
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
new Sortable(wrapper.getElementsByClassName("control-input-wrapper")[0], {
|
||||
handle: '.sortable-handle',
|
||||
draggable: '.sortable',
|
||||
onUpdate: () => {
|
||||
me.update_fields();
|
||||
me.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add_new_fields() {
|
||||
let me = this;
|
||||
|
||||
let fields_html = me.dialog.get_field("fields_html");
|
||||
let add_new_fields = fields_html.$wrapper[0].getElementsByClassName("add-new-fields")[0];
|
||||
add_new_fields.onclick = () => me.column_selector();
|
||||
}
|
||||
|
||||
setup_remove_fields() {
|
||||
let me = this;
|
||||
|
||||
let fields_html = me.dialog.get_field("fields_html");
|
||||
let remove_fields = fields_html.$wrapper[0].getElementsByClassName("remove-field");
|
||||
|
||||
for (let idx = 0; idx < remove_fields.length; idx++) {
|
||||
remove_fields.item(idx).onclick = () => me.remove_fields(remove_fields.item(idx).getAttribute("data-fieldname"));
|
||||
}
|
||||
}
|
||||
|
||||
remove_fields(fieldname) {
|
||||
let me = this;
|
||||
let existing_fields = me.fields.map(f => f.fieldname);
|
||||
|
||||
for (let idx in me.fields) {
|
||||
let field = me.fields[idx];
|
||||
|
||||
if (field.fieldname == fieldname) {
|
||||
me.fields.splice(idx, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
me.set_removed_fields(me.get_removed_listview_fields(me.fields.map(f => f.fieldname), existing_fields));
|
||||
me.refresh();
|
||||
me.update_fields();
|
||||
}
|
||||
|
||||
update_fields() {
|
||||
let me = this;
|
||||
|
||||
let fields_html = me.dialog.get_field("fields_html");
|
||||
let wrapper = fields_html.$wrapper[0];
|
||||
|
||||
let fields_order = wrapper.getElementsByClassName("fields_order");
|
||||
me.fields = [];
|
||||
|
||||
for (let idx = 0; idx < fields_order.length; idx++) {
|
||||
me.fields.push({
|
||||
fieldname: fields_order.item(idx).getAttribute("data-fieldname"),
|
||||
label: fields_order.item(idx).getAttribute("data-label")
|
||||
});
|
||||
}
|
||||
|
||||
me.dialog.set_value("fields", JSON.stringify(me.fields));
|
||||
me.dialog.get_value("fields");
|
||||
}
|
||||
|
||||
column_selector() {
|
||||
let me = this;
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("{0} Fields", [__(me.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
label: __("Reset Fields"),
|
||||
fieldtype: "Button",
|
||||
fieldname: "reset_fields",
|
||||
click: () => me.reset_listview_fields(d)
|
||||
},
|
||||
{
|
||||
label: __("Select Fields"),
|
||||
fieldtype: "MultiCheck",
|
||||
fieldname: "fields",
|
||||
options: me.get_doctype_fields(me.meta, me.fields.map(f => f.fieldname)),
|
||||
columns: 2
|
||||
}
|
||||
]
|
||||
});
|
||||
d.set_primary_action(__('Save'), () => {
|
||||
let values = d.get_values().fields;
|
||||
|
||||
me.set_removed_fields(me.get_removed_listview_fields(values, me.fields.map(f => f.fieldname)));
|
||||
|
||||
me.fields = [];
|
||||
me.set_subject_field(me.meta);
|
||||
me.set_status_field();
|
||||
|
||||
for (let idx in values) {
|
||||
let value = values[idx];
|
||||
|
||||
if (me.fields.length === parseInt(me.dialog.get_values().total_fields)) {
|
||||
break;
|
||||
} else if (value != me.subject_field.fieldname) {
|
||||
let field = frappe.meta.get_docfield(me.doctype, value);
|
||||
if (field) {
|
||||
me.fields.push({
|
||||
label: field.label,
|
||||
fieldname: field.fieldname
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
me.refresh();
|
||||
me.dialog.set_value("fields", JSON.stringify(me.fields));
|
||||
d.hide();
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
|
||||
reset_listview_fields(dialog) {
|
||||
let me = this;
|
||||
|
||||
frappe.xcall("frappe.desk.doctype.list_view_settings.list_view_settings.get_default_listview_fields", {
|
||||
doctype: me.doctype
|
||||
}).then((fields) => {
|
||||
let field = dialog.get_field("fields");
|
||||
field.df.options = me.get_doctype_fields(me.meta, fields);
|
||||
dialog.refresh();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
get_listview_fields(meta) {
|
||||
let me = this;
|
||||
|
||||
if (!me.settings.fields) {
|
||||
me.set_list_view_fields(meta);
|
||||
} else {
|
||||
me.fields = JSON.parse(this.settings.fields);
|
||||
}
|
||||
|
||||
me.fields.uniqBy(f => f.fieldname);
|
||||
}
|
||||
|
||||
set_list_view_fields(meta) {
|
||||
let me = this;
|
||||
|
||||
me.set_subject_field(meta);
|
||||
me.set_status_field();
|
||||
|
||||
meta.fields.forEach(field => {
|
||||
if (field.in_list_view && !in_list(frappe.model.no_value_type, field.fieldtype) &&
|
||||
me.subject_field.fieldname != field.fieldname) {
|
||||
|
||||
me.fields.push({
|
||||
label: field.label,
|
||||
fieldname: field.fieldname
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set_subject_field(meta) {
|
||||
let me = this;
|
||||
|
||||
me.subject_field = {
|
||||
label: "Name",
|
||||
fieldname: "name"
|
||||
};
|
||||
|
||||
if (meta.title_field) {
|
||||
let field = frappe.meta.get_docfield(me.doctype, meta.title_field.trim());
|
||||
|
||||
me.subject_field = {
|
||||
label: field.label,
|
||||
fieldname: field.fieldname
|
||||
};
|
||||
}
|
||||
|
||||
me.fields.push(me.subject_field);
|
||||
}
|
||||
|
||||
set_status_field() {
|
||||
let me = this;
|
||||
|
||||
if (frappe.has_indicator(me.doctype)) {
|
||||
me.fields.push({
|
||||
type: "Status",
|
||||
label: "Status",
|
||||
fieldname: "status_field"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_doctype_fields(meta, fields) {
|
||||
let multiselect_fields = [];
|
||||
|
||||
meta.fields.forEach(field => {
|
||||
if (!in_list(frappe.model.no_value_type, field.fieldtype)) {
|
||||
multiselect_fields.push({
|
||||
label: field.label,
|
||||
value: field.fieldname,
|
||||
checked: in_list(fields, field.fieldname)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return multiselect_fields;
|
||||
}
|
||||
|
||||
get_removed_listview_fields(new_fields, existing_fields) {
|
||||
let me = this;
|
||||
let removed_fields = [];
|
||||
|
||||
if (frappe.has_indicator(me.doctype)) {
|
||||
new_fields.push("status_field");
|
||||
}
|
||||
|
||||
existing_fields.forEach(column => {
|
||||
if (!in_list(new_fields, column)) {
|
||||
removed_fields.push(column);
|
||||
}
|
||||
});
|
||||
|
||||
return removed_fields;
|
||||
}
|
||||
|
||||
set_removed_fields(fields) {
|
||||
let me = this;
|
||||
|
||||
if (me.removed_fields) {
|
||||
me.removed_fields.concat(fields);
|
||||
} else {
|
||||
me.removed_fields = fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue