diff --git a/.snyk b/.snyk
index b39169dcee..0dfecc6136 100644
--- a/.snyk
+++ b/.snyk
@@ -1,5 +1,5 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
-version: v1.13.5
+version: v1.14.1
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JS-AWESOMPLETE-174474:
@@ -22,3 +22,44 @@ patch:
SNYK-JS-LODASH-450202:
- frappe-datatable > lodash:
patched: '2020-01-31T01:33:09.889Z'
+ SNYK-JS-LODASH-567746:
+ - frappe-datatable > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - quagga > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - tailwindcss > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - '@tailwindcss/ui > @tailwindcss/custom-forms > lodash':
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/dep-graph > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > inquirer > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-config > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-mvn-plugin > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-nodejs-lockfile-parser > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-nuget-plugin > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/dep-graph > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-go-plugin > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-nuget-plugin > dotnet-deps-parser > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-php-plugin > @snyk/composer-lockfile-parser > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/ruby-semver > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
diff --git a/.travis.yml b/.travis.yml
index e9c2ee5262..9fab56188b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,5 @@
language: python
-dist: trusty
+dist: bionic
addons:
hosts:
@@ -9,6 +9,10 @@ addons:
postgresql: 9.5
chrome: stable
+services:
+ - xvfb
+ - mysql
+
git:
depth: 1
@@ -21,20 +25,21 @@ cache:
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
- ~/.cache
+
matrix:
include:
- - name: "Python 3.6 MariaDB"
- python: 3.6
+ - name: "Python 3.7 MariaDB"
+ python: 3.7
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage
- - name: "Python 3.6 PostgreSQL"
- python: 3.6
+ - name: "Python 3.7 PostgreSQL"
+ python: 3.7
env: DB=postgres TYPE=server
script: bench --site test_site run-tests --coverage
- name: "Cypress"
- python: 3.6
+ python: 3.7
env: DB=mariadb TYPE=ui
before_script:
- bench --site test_site execute frappe.utils.install.complete_setup_wizard
@@ -42,7 +47,26 @@ matrix:
script: bench --site test_site run-ui-tests frappe --headless
before_install:
- # install wkhtmltopdf
+ # do we really want to run travis?
+ - |
+ ONLY_DOCS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.(md|png|jpg|jpeg)$|^.github|LICENSE' ; echo $?)
+ ONLY_JS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.js$' ; echo $?)
+ ONLY_PY_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | 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
+
+ # 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
@@ -98,7 +122,13 @@ install:
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
- - bench setup requirements --node
+ - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
+
+ # install node-sass which is required for website theme test
+ - cd ./apps/frappe
+ - yarn add node-sass@4.13.1
+ - cd ../..
+
- bench start &
- bench --site test_site reinstall --yes
- bench --site test_site_producer reinstall --yes
diff --git a/cypress.json b/cypress.json
index ae0c45c3ae..97ac41bb61 100644
--- a/cypress.json
+++ b/cypress.json
@@ -2,6 +2,6 @@
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv",
"adminPassword": "admin",
- "defaultCommandTimeout": 10000,
+ "defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000
}
diff --git a/cypress/integration/control_duration.js b/cypress/integration/control_duration.js
new file mode 100644
index 0000000000..f304abd3d9
--- /dev/null
+++ b/cypress/integration/control_duration.js
@@ -0,0 +1,45 @@
+context('Control Duration', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/desk#workspace/Website');
+ });
+
+ function get_dialog_with_duration(show_days=1, show_seconds=1) {
+ return cy.dialog({
+ title: 'Duration',
+ fields: [{
+ 'fieldname': 'duration',
+ 'fieldtype': 'Duration',
+ 'show_seconds': show_days,
+ 'show_days': show_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(0, 0).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');
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js
index 658a7fe320..0dc7d5b88e 100644
--- a/cypress/integration/control_link.js
+++ b/cypress/integration/control_link.js
@@ -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 });
diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js
index 51cba94a70..47f8efe94b 100644
--- a/cypress/integration/list_view_settings.js
+++ b/cypress/integration/list_view_settings.js
@@ -9,7 +9,9 @@ context('List View Settings', () => {
cy.get('.sidebar-stat').should('contain', "Tags");
});
it('disable count and sidebar stats then verify', () => {
+ cy.wait(300);
cy.visit('/desk#List/DocType/List');
+ cy.wait(300);
cy.get('.list-count').should('contain', "20 of");
cy.get('button').contains('Menu').click();
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
diff --git a/cypress/integration/login.js b/cypress/integration/login.js
index 3f13130b58..861377444c 100644
--- a/cypress/integration/login.js
+++ b/cypress/integration/login.js
@@ -21,6 +21,15 @@ context('Login', () => {
cy.location('pathname').should('eq', '/login');
});
+ it('shows invalid login if incorrect credentials', () => {
+ cy.get('#login_email').type('Administrator');
+ cy.get('#login_password').type('qwer');
+
+ cy.get('.btn-login').click();
+ cy.get('.page-card-head').contains('Invalid Login. Try again.');
+ cy.location('pathname').should('eq', '/login');
+ });
+
it('logs in using correct credentials', () => {
cy.get('#login_email').type('Administrator');
cy.get('#login_password').type(Cypress.config('adminPassword'));
@@ -30,12 +39,30 @@ context('Login', () => {
cy.window().its('frappe.session.user').should('eq', 'Administrator');
});
- it('shows invalid login if incorrect credentials', () => {
+ it('check redirect after login', () => {
+
+ // mock for OAuth 2.0 client_id, redirect_uri, scope and state
+ const payload = new URLSearchParams({
+ uuid: '6fed1519-cfd8-4a2d-84a6-9a1799c7c741',
+ encoded_string: 'hello all',
+ encoded_url: 'http://test.localhost/callback',
+ base64_string: 'aGVsbG8gYWxs'
+ });
+
+ cy.request('/api/method/logout');
+
+ // redirect-to /me page with params to mock OAuth 2.0 like request
+ cy.visit(
+ '/login?redirect-to=/me?' +
+ encodeURIComponent(payload.toString().replace("+", " "))
+ );
+
cy.get('#login_email').type('Administrator');
- cy.get('#login_password').type('qwer');
+ cy.get('#login_password').type(Cypress.config('adminPassword'));
cy.get('.btn-login').click();
- cy.get('.page-card-head').contains('Invalid Login. Try again.');
- cy.location('pathname').should('eq', '/login');
+
+ // verify redirected location and url params after login
+ cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));
});
});
diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_time_filters.js
similarity index 85%
rename from cypress/integration/relative_filters.js
rename to cypress/integration/relative_time_filters.js
index 986c5ce342..ac70c44345 100644
--- a/cypress/integration/relative_filters.js
+++ b/cypress/integration/relative_time_filters.js
@@ -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();
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7664ac4c61..8f36c0c4d3 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -23,7 +23,7 @@ if PY2:
reload(sys)
sys.setdefaultencoding("utf-8")
-__version__ = '12.0.0-dev'
+__version__ = '13.0.0-dev'
__title__ = "Frappe Framework"
local = Local()
@@ -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)
@@ -345,7 +344,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
style="margin: 0;">{}'''.format(table_rows)
if flags.print_messages and out.message:
- print("Message: " + repr(out.message).encode("utf-8"))
+ print(f"Message: {repr(out.message).encode('utf-8')}")
if title:
out.title = title
@@ -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'''
diff --git a/frappe/app.py b/frappe/app.py
index 41798b0bc4..50d09177d6 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -26,6 +26,7 @@ from frappe.core.doctype.comment.comment import update_comments_in_parent_after_
from frappe import _
import frappe.recorder
import frappe.monitor
+import frappe.rate_limiter
local_manager = LocalManager([frappe.local])
@@ -54,6 +55,7 @@ def application(request):
frappe.recorder.record()
frappe.monitor.start()
+ frappe.rate_limiter.apply()
if frappe.local.form_dict.cmd:
response = frappe.handler.handle()
@@ -93,9 +95,23 @@ def application(request):
if response and hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)
+ frappe.rate_limiter.update()
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())
+
frappe.destroy()
return response
@@ -171,6 +187,9 @@ def handle_exception(e):
http_status_code=http_status_code, indicator_color='red')
return_as_message = True
+ elif http_status_code == 429:
+ response = frappe.rate_limiter.respond()
+
else:
traceback = "
+ $(`
`).appendTo(this.wrapper.find(".page-content").empty());
this.container = this.wrapper.find(".dashboard-graph");
this.page = wrapper.page;
+
+ this.page.set_title_sub(
+ $(`
+
+ ${__('Restricted')}
+ `)
+ );
}
show() {
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 394f38b56c..77490c8c43 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -16,6 +16,8 @@
"column_break_6",
"fieldtype",
"precision",
+ "show_seconds",
+ "show_days",
"options",
"fetch_from",
"fetch_if_empty",
@@ -48,6 +50,7 @@
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
+ "hide_border",
"description",
"permlevel",
"width",
@@ -55,361 +58,386 @@
],
"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": "1",
+ "depends_on": "eval:doc.fieldtype === \"Duration\";",
+ "fieldname": "show_seconds",
+ "fieldtype": "Check",
+ "label": "Show Seconds",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:doc.fieldtype === \"Duration\";",
+ "fieldname": "show_days",
+ "fieldtype": "Check",
+ "label": "Show Days",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "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-10 11:57:10.392218",
+ "modified": "2020-05-15 23:43:00.123572",
"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",
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index 21679c5bc7..a24777a80a 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -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
diff --git a/frappe/website/web_template/footer_horizontal/__init__.py b/frappe/custom/doctype/custom_link/__init__.py
similarity index 100%
rename from frappe/website/web_template/footer_horizontal/__init__.py
rename to frappe/custom/doctype/custom_link/__init__.py
diff --git a/frappe/custom/doctype/custom_link/custom_link.js b/frappe/custom/doctype/custom_link/custom_link.js
new file mode 100644
index 0000000000..8662724b1a
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/custom_link.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Custom Link', {
+ refresh: function(frm) {
+ frm.set_query("document_type", function () {
+ return {
+ filters: {
+ custom: 0,
+ istable: 0,
+ module: ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
+ }
+ };
+ });
+
+ frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), function() {
+ frappe.set_route('List', frm.doc.document_type);
+ });
+ }
+});
diff --git a/frappe/custom/doctype/custom_link/custom_link.json b/frappe/custom/doctype/custom_link/custom_link.json
new file mode 100644
index 0000000000..350e6b1c2d
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/custom_link.json
@@ -0,0 +1,52 @@
+{
+ "actions": [],
+ "autoname": "field:document_type",
+ "creation": "2020-04-08 15:16:44.342509",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "links"
+ ],
+ "fields": [
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "DocType Link"
+ }
+ ],
+ "links": [],
+ "modified": "2020-04-08 16:42:59.402671",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Link",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.py b/frappe/custom/doctype/custom_link/custom_link.py
similarity index 66%
rename from frappe/desk/doctype/list_view_setting/list_view_setting.py
rename to frappe/custom/doctype/custom_link/custom_link.py
index b66dc29a43..11316d5751 100644
--- a/frappe/desk/doctype/list_view_setting/list_view_setting.py
+++ b/frappe/custom/doctype/custom_link/custom_link.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2019, Frappe Technologies and contributors
+# 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 ListViewSetting(Document):
+class CustomLink(Document):
pass
diff --git a/frappe/desk/doctype/onboarding/test_onboarding.py b/frappe/custom/doctype/custom_link/test_custom_link.py
similarity index 81%
rename from frappe/desk/doctype/onboarding/test_onboarding.py
rename to frappe/custom/doctype/custom_link/test_custom_link.py
index 8a9e346fd9..a292f73ad0 100644
--- a/frappe/desk/doctype/onboarding/test_onboarding.py
+++ b/frappe/custom/doctype/custom_link/test_custom_link.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
-class TestOnboarding(unittest.TestCase):
+class TestCustomLink(unittest.TestCase):
pass
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index ebf01d11b3..6a54d9c7e6 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -76,7 +76,8 @@ docfield_properties = {
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link',
- 'allow_in_quick_entry': 'Check'
+ 'allow_in_quick_entry': 'Check',
+ 'hide_border': 'Check'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index d7887cf8bd..f422c36e61 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -11,6 +11,8 @@
"label",
"fieldtype",
"fieldname",
+ "show_seconds",
+ "show_days",
"reqd",
"unique",
"in_list_view",
@@ -39,6 +41,7 @@
"allow_on_submit",
"report_hide",
"remember_last_selected_value",
+ "hide_border",
"property_depends_on_section",
"mandatory_depends_on",
"column_break_33",
@@ -57,343 +60,368 @@
],
"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": "1",
+ "depends_on": "eval:doc.fieldtype === \"Duration\";",
+ "fieldname": "show_seconds",
+ "fieldtype": "Check",
+ "label": "Show Seconds",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:doc.fieldtype === \"Duration\";",
+ "fieldname": "show_days",
+ "fieldtype": "Check",
+ "label": "Show Days",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "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-10 11:58:44.573537",
+ "modified": "2020-05-15 23:45:46.810869",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/website/web_template/navbar_with_links_on_right/__init__.py b/frappe/custom/doctype/package_document_type/__init__.py
similarity index 100%
rename from frappe/website/web_template/navbar_with_links_on_right/__init__.py
rename to frappe/custom/doctype/package_document_type/__init__.py
diff --git a/frappe/custom/doctype/package_document_type/package_document_type.json b/frappe/custom/doctype/package_document_type/package_document_type.json
new file mode 100644
index 0000000000..6d011bd4e4
--- /dev/null
+++ b/frappe/custom/doctype/package_document_type/package_document_type.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "creation": "2020-05-14 16:45:47.196395",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "column_break_2",
+ "attachments",
+ "overwrite",
+ "section_break_4",
+ "filters_json"
+ ],
+ "fields": [
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "attachments",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Include Attachments"
+ },
+ {
+ "default": "0",
+ "fieldname": "overwrite",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Overwrite"
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "filters_json",
+ "fieldtype": "Code",
+ "label": "Filters",
+ "options": "JSON"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-14 16:45:47.196395",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Package Document Type",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_document_type/package_document_type.py b/frappe/custom/doctype/package_document_type/package_document_type.py
new file mode 100644
index 0000000000..6e166eecbd
--- /dev/null
+++ b/frappe/custom/doctype/package_document_type/package_document_type.py
@@ -0,0 +1,10 @@
+# -*- 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 PackageDocumentType(Document):
+ pass
diff --git a/frappe/website/web_template/section_with_left_image/__init__.py b/frappe/custom/doctype/package_publish_target/__init__.py
similarity index 100%
rename from frappe/website/web_template/section_with_left_image/__init__.py
rename to frappe/custom/doctype/package_publish_target/__init__.py
diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.json b/frappe/custom/doctype/package_publish_target/package_publish_target.json
new file mode 100644
index 0000000000..baeb7cb8bc
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_target/package_publish_target.json
@@ -0,0 +1,47 @@
+{
+ "actions": [],
+ "creation": "2020-05-13 16:04:32.724663",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "instance_url",
+ "username",
+ "password"
+ ],
+ "fields": [
+ {
+ "fieldname": "instance_url",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Site URL",
+ "reqd": 1
+ },
+ {
+ "fieldname": "username",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Username",
+ "reqd": 1
+ },
+ {
+ "fieldname": "password",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "Password",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-15 17:35:16.282235",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Package Publish Target",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.py b/frappe/custom/doctype/package_publish_target/package_publish_target.py
new file mode 100644
index 0000000000..34eee02562
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_target/package_publish_target.py
@@ -0,0 +1,10 @@
+# -*- 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 PackagePublishTarget(Document):
+ pass
diff --git a/frappe/custom/doctype/package_publish_tool/__init__.py b/frappe/custom/doctype/package_publish_tool/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js
new file mode 100644
index 0000000000..a0190a8d8c
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js
@@ -0,0 +1,159 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Publish Tool', {
+ refresh: function(frm) {
+ frm.set_query("document_type", "package_details", function () {
+ return {
+ filters: {
+ "istable": 0,
+ }
+ };
+ });
+
+ frappe.realtime.on("package", (data) => {
+ frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message]));
+ if ((data.progress+1) != data.total) {
+ frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message]));
+ } else {
+ frm.dashboard.hide_progress();
+ }
+ });
+
+ frm.trigger("show_instructions");
+ frm.trigger("last_deployed_on");
+ frm.trigger("set_dirty_trigger");
+ frm.trigger("set_deploy_primary_action");
+ },
+ last_deployed_on: function(frm) {
+ if (frm.doc.last_deployed_on) {
+ frm.trigger("show_indicator");
+ }
+ },
+ show_indicator: function(frm) {
+ let pretty_date = frappe.datetime.prettyDate(frm.doc.last_deployed_on);
+ frm.page.set_indicator(__("Last published {0}", [pretty_date]), "blue");
+ },
+ set_dirty_trigger: function(frm) {
+ $(frm.wrapper).on("dirty", function() {
+ frm.page.set_primary_action(__('Save'), () => frm.save());
+ });
+ },
+ set_deploy_primary_action: function(frm) {
+ if (frm.doc.package_details.length && frm.doc.instances.length) {
+ frm.page.set_primary_action(__("Publish"), function () {
+ frappe.show_alert({
+ message: __("Publishing documents..."),
+ indicator: "green"
+ });
+
+ frappe.call({
+ method: "frappe.custom.doctype.package_publish_tool.package_publish_tool.deploy_package",
+ callback: function() {
+ frm.reload_doc();
+ frappe.msgprint(__("Documents have been published."));
+ }
+ });
+ });
+ }
+ },
+ show_instructions: function(frm) {
+ let field = frm.get_field("html_info");
+ field.html(`
+
+ Package Publish Tool let's you copy documents from your site to any other remote site.
+ Follow the steps below to publish.
+
+
+ Add Document Types that you want to copy from the table below. You can also add filters by expanding the row.
+ Add the Sites URL where you want to copy these documents, and enter the Username and Password.
+ Click on Save. Now, you can click on Publish and the documents will be copied.
+
+ `);
+ }
+});
+
+frappe.ui.form.on('Package Document Type', {
+ form_render: function (frm, cdt, cdn) {
+ function _show_filters(filters, table) {
+ table.find('tbody').empty();
+
+ if (filters.length > 0) {
+ filters.forEach(filter => {
+ const filter_row =
+ $(`
+ ${filter[1]}
+ ${filter[2] || ""}
+ ${filter[3]}
+ `);
+
+ table.find('tbody').append(filter_row);
+ });
+ } else {
+ const filter_row = $(`
+ ${__("Click to Set Filters")} `);
+ table.find('tbody').append(filter_row);
+ }
+ }
+
+ let row = frappe.get_doc(cdt, cdn);
+
+ let wrapper = $(`[data-fieldname="filters_json"]`).empty();
+ let table = $(`
+
+
+ ${__('Filter')}
+ ${__('Condition')}
+ ${__('Value')}
+
+
+
+
+
`).appendTo(wrapper);
+ $(`
${__("Click table to edit")}
`).appendTo(wrapper);
+
+ let filters = JSON.parse(row.filters_json || '[]');
+ _show_filters(filters, table);
+
+ table.on('click', () => {
+ if (!row.document_type) {
+ frappe.msgprint(__("Select Document Type."));
+ return;
+ }
+
+ frappe.model.with_doctype(row.document_type, function() {
+ let dialog = new frappe.ui.Dialog({
+ title: __('Set Filters'),
+ fields: [
+ {
+ fieldtype: 'HTML',
+ label: 'Filters',
+ fieldname: 'filter_area',
+ }
+ ],
+ primary_action: function() {
+ let values = filter_group.get_filters();
+ let flt = [];
+ if (values) {
+ values.forEach(function(value) {
+ flt.push([value[0], value[1], value[2], value[3]]);
+ });
+ }
+ row.filters_json = JSON.stringify(flt);
+ _show_filters(flt, table);
+ dialog.hide();
+ },
+ primary_action_label: "Set"
+ });
+
+ let filter_group = new frappe.ui.FilterGroup({
+ parent: dialog.get_field('filter_area').$wrapper,
+ doctype: row.document_type,
+ on_change: () => {},
+ });
+ filter_group.add_filters_to_filter_group(filters);
+ dialog.show();
+ });
+ });
+ },
+});
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json
new file mode 100644
index 0000000000..0f85ae0348
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json
@@ -0,0 +1,84 @@
+{
+ "actions": [],
+ "creation": "2020-05-13 15:54:38.082657",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "html_info",
+ "sb_00",
+ "package_details",
+ "sb_01",
+ "instances",
+ "last_deployed_on"
+ ],
+ "fields": [
+ {
+ "description": "Click on the row for accessing filters.",
+ "fieldname": "package_details",
+ "fieldtype": "Table",
+ "label": "Document Types",
+ "options": "Package Document Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "instances",
+ "fieldtype": "Table",
+ "label": "Sites",
+ "options": "Package Publish Target",
+ "reqd": 1
+ },
+ {
+ "fieldname": "html_info",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "last_deployed_on",
+ "fieldtype": "Datetime",
+ "hidden": 1,
+ "label": "Last Deployed On",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sb_00",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "sb_01",
+ "fieldtype": "Section Break"
+ }
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-05-15 17:31:37.060199",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Package Publish Tool",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py
new file mode 100644
index 0000000000..a01dd0ba47
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+import datetime
+import base64
+from frappe.model.document import Document
+from frappe.utils.file_manager import save_file, get_file
+from frappe import _
+from six import string_types
+from frappe.frappeclient import FrappeClient
+from frappe.utils import get_datetime_str, get_datetime
+from frappe.utils.password import get_decrypted_password
+
+class PackagePublishTool(Document):
+ pass
+
+@frappe.whitelist()
+def deploy_package():
+ package, doc = export_package()
+
+ file_name = "Package-" + get_datetime_str(get_datetime())
+
+ length = len(doc.instances)
+ for idx, instance in enumerate(doc.instances):
+ frappe.publish_realtime("package", {"progress": idx, "total": length, "message": instance.instance_url, "prefix": _("Deploying")},
+ user=frappe.session.user)
+
+ install_package_to_remote(package, instance)
+
+ frappe.db.set_value("Package Publish Tool", "Package Publish Tool", "last_deployed_on", frappe.utils.now_datetime())
+
+def install_package_to_remote(package, instance):
+ try:
+ connection = FrappeClient(instance.instance_url, instance.username, get_decrypted_password(instance.doctype, instance.name))
+ except Exception:
+ frappe.log_error(frappe.get_traceback())
+ frappe.throw(_("Couldn't connect to site {0}. Please check Error Logs.").format(instance.instance_url))
+
+ try:
+ connection.post_request({
+ "cmd": "frappe.custom.doctype.package_publish_tool.package_publish_tool.import_package",
+ "package": json.dumps(package)
+ })
+ except Exception:
+ frappe.log_error(frappe.get_traceback())
+ frappe.throw(_("Error while installing package to site {0}. Please check Error Logs.").format(instance.instance_url))
+
+@frappe.whitelist()
+def export_package():
+ """Export package as JSON."""
+ package_doc = frappe.get_single("Package Publish Tool")
+ package = []
+
+ for doctype in package_doc.package_details:
+ filters = []
+
+ if doctype.get("filters_json"):
+ filters = json.loads(doctype.get("filters_json"))
+
+ docs = frappe.get_all(doctype.get("document_type"), filters=filters)
+ length = len(docs)
+
+ for idx, doc in enumerate(docs):
+ frappe.publish_realtime("package", {
+ "progress":idx, "total":length,
+ "message":doctype.get("document_type"),
+ "prefix": _("Exporting")
+ },
+ user=frappe.session.user)
+
+ document = frappe.get_doc(doctype.get("document_type"), doc.name).as_dict()
+ attachments = []
+
+ if doctype.attachments:
+ filters = {
+ "attached_to_doctype": document.get("doctype"),
+ "attached_to_name": document.get("name")
+ }
+
+ for f in frappe.get_list("File", filters=filters):
+ fname, fcontents = get_file(f.name)
+ attachments.append({
+ "fname": fname,
+ "content": base64.b64encode(fcontents).decode('ascii')
+ })
+
+ document.update({
+ "__attachments": attachments,
+ "__overwrite": True if doctype.overwrite else False
+ })
+
+ package.append(document)
+
+ return post_process(package), package_doc
+
+@frappe.whitelist()
+def import_package(package=None):
+ """Import package from JSON."""
+ if isinstance(package, string_types):
+ package = json.loads(package)
+
+ for doc in package:
+ modified = doc.pop("modified")
+ overwrite = doc.pop("__overwrite")
+ attachments = doc.pop("__attachments")
+ exists = frappe.db.exists(doc.get("doctype"), doc.get("name"))
+
+ if not exists:
+ d = frappe.get_doc(doc).insert(ignore_permissions=True, ignore_if_duplicate=True)
+ if attachments:
+ add_attachment(attachments, d)
+ else:
+ docname = doc.pop("name")
+ document = frappe.get_doc(doc.get("doctype"), docname)
+
+ if overwrite:
+ update_document(document, doc, attachments)
+
+ else:
+ if frappe.utils.get_datetime(document.modified) < frappe.utils.get_datetime(modified):
+ update_document(document, doc, attachments)
+
+def update_document(document, doc, attachments):
+ document.update(doc)
+ document.save()
+ if attachments:
+ add_attachment(attachments, document)
+
+def add_attachment(attachments, doc):
+ for attachment in attachments:
+ save_file(attachment.get("fname"), base64.b64decode(attachment.get("content")), doc.get("doctype"), doc.get("name"))
+
+def post_process(package):
+ """Remove the keys from Document and Child Document. Convert datetime, date, time to str."""
+ del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus')
+ child_del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus', 'name')
+
+ for doc in package:
+ for key in del_keys:
+ if key in doc:
+ del doc[key]
+
+ for key, value in doc.items():
+ stringified_value = get_stringified_value(value)
+ if stringified_value:
+ doc[key] = stringified_value
+
+ if not isinstance(value, list):
+ continue
+
+ for child in value:
+ for child_key in child_del_keys:
+ if child_key in child:
+ del child[child_key]
+
+ for child_key, child_value in child.items():
+ stringified_value = get_stringified_value(child_value)
+ if stringified_value:
+ child[child_key] = stringified_value
+
+ return package
+
+def get_stringified_value(value):
+ if isinstance(value, datetime.datetime):
+ return frappe.utils.get_datetime_str(value)
+
+ if isinstance(value, datetime.date):
+ return frappe.utils.get_date_str(value)
+
+ if isinstance(value, datetime.timedelta):
+ return frappe.utils.get_time_str(value)
+
+ return None
diff --git a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py
new file mode 100644
index 0000000000..8332240543
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestPackagePublishTool(unittest.TestCase):
+ pass
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index cd053569f0..4ec89c126d 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -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):
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 46940cc846..bd93069a3f 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -63,6 +63,7 @@ CREATE TABLE `tabDocField` (
`precision` varchar(255) DEFAULT NULL,
`length` int(11) NOT NULL DEFAULT 0,
`translatable` int(1) NOT NULL DEFAULT 0,
+ `hide_border` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `label` (`label`),
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index e30ef3293f..e348916705 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -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):
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 26760dbcc9..76309e7347 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -63,6 +63,7 @@ CREATE TABLE "tabDocField" (
"precision" varchar(255) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
+ "hide_border" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index f2047003fa..6ca101c3a8 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -21,19 +21,17 @@ class Workspace:
self.extended_charts = []
self.extended_shortcuts = []
- user = frappe.get_user()
- user.build_permissions()
-
- user_doc = frappe.get_doc('User', frappe.session.user)
- self.blocked_modules = user_doc.get_blocked_modules()
+ self.user = frappe.get_user()
+ self.allowed_modules = self.get_cached_value('user_allowed_modules', self.get_allowed_modules)
self.doc = self.get_page_for_user()
- if self.doc.module in self.blocked_modules:
+ if self.doc.module not in self.allowed_modules:
raise frappe.PermissionError
- self.user = user
- self.allowed_pages = get_allowed_pages()
- self.allowed_reports = get_allowed_reports()
+ self.can_read = self.get_cached_value('user_perm_can_read', self.get_can_read_items)
+
+ self.allowed_pages = get_allowed_pages(cache=True)
+ self.allowed_reports = get_allowed_reports(cache=True)
self.onboarding_doc = self.get_onboarding_doc()
self.onboarding = None
@@ -41,6 +39,31 @@ class Workspace:
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
+ def get_cached_value(self, cache_key, fallback_fn):
+ _cache = frappe.cache()
+
+ value = _cache.get_value(cache_key, user=frappe.session.user)
+ if value:
+ return value
+
+ value = fallback_fn()
+
+ # Expire every six hour
+ _cache.set_value(cache_key, value, frappe.session.user, 21600)
+ return value
+
+ def get_can_read_items(self):
+ if not self.user.can_read:
+ self.user.build_permissions()
+
+ return self.user.can_read
+
+ def get_allowed_modules(self):
+ if not self.user.allow_modules:
+ self.user.build_permissions()
+
+ return self.user.allow_modules
+
def get_page_for_user(self):
filters = {
'extends': self.page_name,
@@ -61,14 +84,14 @@ class Workspace:
if not self.doc.onboarding:
return None
- if frappe.db.get_value("Onboarding", self.doc.onboarding, "is_complete"):
+ if frappe.db.get_value("Module Onboarding", self.doc.onboarding, "is_complete"):
return None
- doc = frappe.get_doc("Onboarding", self.doc.onboarding)
+ doc = frappe.get_doc("Module Onboarding", self.doc.onboarding)
# Check if user is allowed
allowed_roles = set(doc.get_allowed_roles())
- user_roles = set(self.user.get_roles())
+ user_roles = set(frappe.get_roles())
if not allowed_roles & user_roles:
return None
@@ -83,7 +106,7 @@ class Workspace:
"extends": self.page_name,
'restrict_to_domain': ['in', frappe.get_active_domains()],
'for_user': '',
- 'module': ['not in', self.blocked_modules]
+ 'module': ['in', self.allowed_modules]
})
pages = [frappe.get_doc("Desk Page", page['name']) for page in pages]
@@ -97,13 +120,15 @@ class Workspace:
item_type = item_type.lower()
if item_type == "doctype":
- return (name in self.user.can_read and name in self.restricted_doctypes)
+ return (name in self.can_read and name in self.restricted_doctypes)
if item_type == "page":
return (name in self.allowed_pages and name in self.restricted_pages)
if item_type == "report":
return name in self.allowed_reports
if item_type == "help":
return True
+ if item_type == "dashboard":
+ return True
return False
@@ -134,15 +159,18 @@ class Workspace:
}
def get_cards(self):
- cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module)
+ cards = self.doc.cards
+ if not self.doc.hide_custom:
+ cards = cards + get_custom_reports_and_doctypes(self.doc.module)
+
if len(self.extended_cards):
cards = cards + self.extended_cards
default_country = frappe.db.get_default("country")
def _doctype_contains_a_record(name):
- exists = self.table_counts.get(name)
- if not exists:
- if not frappe.db.get_value('DocType', name, 'issingle'):
+ exists = self.table_counts.get(name, None)
+ if exists is None:
+ if not frappe.db.get_value('DocType', name, 'issingle', cache=True):
exists = frappe.db.count(name)
else:
exists = True
@@ -249,6 +277,8 @@ class Workspace:
for doc in self.onboarding_doc.get_steps():
step = doc.as_dict().copy()
step.label = _(doc.title)
+ if step.action == "Create Entry":
+ step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True)
steps.append(step)
return steps
@@ -292,7 +322,6 @@ def get_desk_sidebar_items(flatten=False):
filters = {
'restrict_to_domain': ['in', frappe.get_active_domains()],
'extends_another_page': 0,
- 'is_standard': 1,
'for_user': '',
'module': ['not in', blocked_modules]
}
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index c17bc3235c..c0e2bddcf8 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -9,6 +9,7 @@
"dashboard_name",
"is_default",
"charts",
+ "chart_options",
"cards"
],
"fields": [
@@ -33,6 +34,13 @@
"options": "Dashboard Chart Link",
"reqd": 1
},
+ {
+ "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
+ "fieldname": "chart_options",
+ "fieldtype": "Code",
+ "label": "Chart Options",
+ "options": "JSON"
+ },
{
"fieldname": "cards",
"fieldtype": "Table",
@@ -41,7 +49,7 @@
}
],
"links": [],
- "modified": "2020-04-19 17:44:36.237163",
+ "modified": "2020-04-29 13:26:37.362482",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index b85e135071..af0c48d9c6 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -5,6 +5,8 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
+from frappe import _
+import json
class Dashboard(Document):
def on_update(self):
@@ -13,13 +15,29 @@ class Dashboard(Document):
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
+ def validate(self):
+ self.validate_custom_options()
+
+ def validate_custom_options(self):
+ if self.chart_options:
+ try:
+ json.loads(self.chart_options)
+ except ValueError as error:
+ frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
+
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
permitted_charts = []
dashboard = frappe.get_doc('Dashboard', dashboard_name)
for chart in dashboard.charts:
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
- permitted_charts.append(chart)
+ chart_dict = frappe._dict()
+ chart_dict.update(chart.as_dict())
+
+ if dashboard.get('chart_options'):
+ chart_dict.custom_options = dashboard.get('chart_options')
+ permitted_charts.append(chart_dict)
+
return permitted_charts
@frappe.whitelist()
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index f8d5886b26..a10d3d96f2 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -49,6 +49,7 @@ frappe.ui.form.on('Dashboard Chart', {
});
frm.set_df_property("filters_section", "hidden", 1);
+ frm.trigger('set_time_series');
frm.set_query('document_type', function() {
return {
filters: {
@@ -57,6 +58,7 @@ frappe.ui.form.on('Dashboard Chart', {
}
});
frm.trigger('update_options');
+ frm.trigger('set_heatmap_year_options');
if (frm.doc.report_name) {
frm.trigger('set_chart_report_filters');
}
@@ -70,7 +72,17 @@ frappe.ui.form.on('Dashboard Chart', {
frm.trigger("show_filters");
},
+ set_heatmap_year_options: function(frm) {
+ if (frm.doc.type == 'Heatmap') {
+ frappe.db.get_doc('System Settings').then(doc => {
+ const creation_date = doc.creation;
+ frm.set_df_property('heatmap_year', 'options', frappe.dashboard_utils.get_years_since_creation(creation_date));
+ });
+ }
+ },
+
chart_type: function(frm) {
+ frm.trigger('set_time_series');
if (frm.doc.chart_type == 'Report') {
frm.set_query('report_name', () => {
return {
@@ -80,23 +92,19 @@ frappe.ui.form.on('Dashboard Chart', {
}
});
} else {
- // set timeseries based on chart type
- if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
- frm.set_value('timeseries', 1);
- } else {
- frm.set_value('timeseries', 0);
- }
-
- if (frm.doc.chart_type == 'Group By') {
- frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie']);
- } else {
- frm.set_df_property('type', 'options', ['Line', 'Bar']);
- }
-
frm.set_value('document_type', '');
}
},
+ set_time_series: function(frm) {
+ // set timeseries based on chart type
+ if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
+ frm.set_value('timeseries', 1);
+ } else {
+ frm.set_value('timeseries', 0);
+ }
+ },
+
document_type: function(frm) {
// update `based_on` options based on date / datetime fields
frm.set_value('source', '');
@@ -243,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 = $(`
@@ -260,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 = [
@@ -283,16 +304,7 @@ frappe.ui.form.on('Dashboard Chart', {
});
}
} else if (frm.chart_filters.length) {
- fields = frm.chart_filters.filter(f => {
- if (f.on_change && !f.reqd) {
- return false;
- }
- if (f.get_query || f.get_data) {
- f.read_only = 1;
- }
-
- return f.fieldname;
- });
+ fields = frm.chart_filters.filter(f => f.fieldname);
fields.map( f => {
if (filters[f.fieldname]) {
@@ -320,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) {
@@ -353,10 +365,17 @@ frappe.ui.form.on('Dashboard Chart', {
}
dialog.show();
+
+ 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);
});
},
});
-
-
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index b5201a8b1f..4bab76337f 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -23,17 +23,18 @@
"number_of_groups",
"column_break_6",
"is_public",
+ "heatmap_year",
"timespan",
"from_date",
"to_date",
"time_interval",
"timeseries",
+ "type",
"filters_section",
"filters_json",
"chart_options_section",
- "type",
- "column_break_2",
"color",
+ "column_break_2",
"custom_options",
"section_break_10",
"last_synced_on"
@@ -85,14 +86,14 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "timeseries",
+ "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'",
"fieldname": "timespan",
"fieldtype": "Select",
"label": "Timespan",
"options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range"
},
{
- "depends_on": "timeseries",
+ "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'",
"fieldname": "time_interval",
"fieldtype": "Select",
"label": "Time Interval",
@@ -100,7 +101,7 @@
},
{
"default": "0",
- "depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
+ "depends_on": "eval: !['Group By', 'Report'].includes(doc.chart_type)\n",
"fieldname": "timeseries",
"fieldtype": "Check",
"label": "Time Series"
@@ -123,18 +124,18 @@
"label": "Chart Options"
},
{
+ "default": "Line",
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
- "options": "Line\nBar\nPercentage\nPie\nDonut",
- "reqd": 1
+ "options": "Line\nBar\nPercentage\nPie\nDonut\nHeatmap"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.chart_type !== 'Report'",
+ "depends_on": "eval: doc.chart_type !== 'Report' && doc.type !== 'Heatmap'",
"fieldname": "color",
"fieldtype": "Color",
"label": "Color"
@@ -228,10 +229,16 @@
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Heatmap'",
+ "fieldname": "heatmap_year",
+ "fieldtype": "Select",
+ "label": "Year"
}
],
"links": [],
- "modified": "2020-05-01 15:22:59.119341",
+ "modified": "2020-05-16 15:03:02.455395",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 7ddb3d98f0..ab1863ca0b 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -7,8 +7,8 @@ import frappe
from frappe import _
import datetime
import json
-from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
-from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime
+from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
+from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime, cint
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
from frappe.model.document import Document
@@ -58,13 +58,13 @@ def has_permission(doc, ptype, user):
@frappe.whitelist()
@cache_source
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
- to_date = None, timespan = None, time_interval = None, refresh = None):
+ to_date = None, timespan = None, time_interval = None, heatmap_year=None, refresh = None):
if chart_name:
chart = frappe.get_doc('Dashboard Chart', chart_name)
else:
chart = frappe._dict(frappe.parse_json(chart))
-
+ heatmap_year = heatmap_year or chart.heatmap_year
timespan = timespan or chart.timespan
if timespan == 'Select Date Range':
@@ -87,7 +87,10 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
if chart.chart_type == 'Group By':
chart_config = get_group_by_chart_config(chart, filters)
else:
- chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date)
+ if chart.type == 'Heatmap':
+ chart_config = get_heatmap_chart_config(chart, filters, heatmap_year)
+ else:
+ chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date)
return chart_config
@@ -107,11 +110,11 @@ def create_dashboard_chart(args):
doc.insert(ignore_permissions=True)
return doc
-
@frappe.whitelist()
def create_report_chart(args):
- create_dashboard_chart(args)
+ doc = create_dashboard_chart(args)
args = frappe.parse_json(args)
+ args.chart_name = doc.chart_name
if args.dashboard:
add_chart_to_dashboard(json.dumps(args))
@@ -134,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'
@@ -144,26 +146,21 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
filters.append([doctype, datefield, '>=', from_date, False])
filters.append([doctype, datefield, '<=', to_date, False])
- data = frappe.db.get_all(
+ 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": [{
@@ -174,6 +171,41 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
return chart_config
+def get_heatmap_chart_config(chart, filters, heatmap_year):
+ aggregate_function = get_aggregate_function(chart.chart_type)
+ value_field = chart.value_based_on or '1'
+ doctype = chart.document_type
+ datefield = chart.based_on
+ year = cint(heatmap_year) if heatmap_year else getdate(nowdate()).year
+ year_start_date = datetime.date(year, 1, 1).strftime('%Y-%m-%d')
+ next_year_start_date = datetime.date(year + 1, 1, 1).strftime('%Y-%m-%d')
+
+ filters.append([doctype, datefield, '>', "{date}".format(date=year_start_date), False])
+ filters.append([doctype, datefield, '<', "{date}".format(date=next_year_start_date), False])
+
+ if frappe.db.db_type == 'mariadb':
+ timestamp_field = 'unix_timestamp({datefield})'.format(datefield=datefield)
+ else:
+ timestamp_field = 'extract(epoch from timestamp {datefield})'.format(datefield=datefield)
+
+ data = dict(frappe.db.get_all(
+ doctype,
+ fields = [
+ timestamp_field,
+ '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
+ ],
+ filters = filters,
+ group_by = 'date({datefield})'.format(datefield=datefield),
+ as_list = 1,
+ order_by = '{datefield} asc'.format(datefield=datefield),
+ ignore_ifnull = True
+ ))
+
+ chart_config = {
+ 'labels': [],
+ 'dataPoints': data,
+ }
+ return chart_config
def get_group_by_chart_config(chart, filters):
@@ -182,7 +214,7 @@ def get_group_by_chart_config(chart, filters):
group_by_field = chart.group_by_based_on
doctype = chart.document_type
- data = frappe.db.get_all(
+ data = frappe.db.get_list(
doctype,
fields = [
'{} as name'.format(group_by_field),
@@ -223,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
@@ -320,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'))
@@ -397,11 +371,11 @@ class DashboardChart(Document):
def check_document_type(self):
if frappe.get_meta(self.document_type).issingle:
- frappe.throw("You cannot create a dashboard chart from single DocTypes")
+ frappe.throw(_("You cannot create a dashboard chart from single DocTypes"))
def validate_custom_options(self):
if self.custom_options:
try:
json.loads(self.custom_options)
except ValueError as error:
- frappe.throw("Invalid json added in the custom options: %s" % error)
\ No newline at end of file
+ frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 4425c4fd45..dfc6edbf58 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -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()
diff --git a/frappe/desk/doctype/desk_page/desk_page.js b/frappe/desk/doctype/desk_page/desk_page.js
index 3087a5f5b8..503859eb61 100644
--- a/frappe/desk/doctype/desk_page/desk_page.js
+++ b/frappe/desk/doctype/desk_page/desk_page.js
@@ -2,16 +2,22 @@
// For license information, please see license.txt
frappe.ui.form.on('Desk Page', {
- setup: function(frm) {
+ refresh: function(frm) {
+ frm.enable_save();
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
frm.get_field("extends_another_page").toggle(frappe.boot.developer_mode);
- if (!frappe.boot.developer_mode || frm.doc.for_user) {
+ frm.get_field("developer_mode_only").toggle(frappe.boot.developer_mode);
+
+ if (frm.doc.for_user) {
+ frm.set_df_property("extends", "read_only", true);
+ }
+
+ if (frm.doc.for_user || (frm.doc.is_standard && !frappe.boot.developer_mode)) {
frm.trigger('disable_form');
}
},
disable_form: function(frm) {
- frm.set_read_only();
frm.fields
.filter(field => field.has_input)
.forEach(field => {
diff --git a/frappe/desk/doctype/desk_page/desk_page.json b/frappe/desk/doctype/desk_page/desk_page.json
index cb106c5dd4..2b8aea5e6c 100644
--- a/frappe/desk/doctype/desk_page/desk_page.json
+++ b/frappe/desk/doctype/desk_page/desk_page.json
@@ -8,8 +8,8 @@
"engine": "InnoDB",
"field_order": [
"label",
- "extends",
"for_user",
+ "extends",
"module",
"category",
"restrict_to_domain",
@@ -21,6 +21,7 @@
"disable_user_customization",
"pin_to_top",
"pin_to_bottom",
+ "hide_custom",
"section_break_2",
"charts_label",
"charts",
@@ -170,7 +171,7 @@
"search_index": 1
},
{
- "depends_on": "eval:doc.extends_another_page == 1",
+ "depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
"fieldname": "extends",
"fieldtype": "Link",
"in_standard_filter": 1,
@@ -188,11 +189,18 @@
"fieldname": "onboarding",
"fieldtype": "Link",
"label": "Onboarding",
- "options": "Onboarding"
+ "options": "Module Onboarding"
+ },
+ {
+ "default": "0",
+ "description": "Checking this will hide custom doctypes and reports cards in Links section",
+ "fieldname": "hide_custom",
+ "fieldtype": "Check",
+ "label": "Hide Custom DocTypes and Reports"
}
],
"links": [],
- "modified": "2020-04-26 12:21:46.205079",
+ "modified": "2020-05-18 19:17:27.206646",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Page",
diff --git a/frappe/desk/doctype/desk_page/desk_page.py b/frappe/desk/doctype/desk_page/desk_page.py
index dd9cc0706a..f14535cb5f 100644
--- a/frappe/desk/doctype/desk_page/desk_page.py
+++ b/frappe/desk/doctype/desk_page/desk_page.py
@@ -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 \
diff --git a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
index 9f8990732a..f3fd546a77 100644
--- a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
+++ b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
@@ -6,9 +6,9 @@
"engine": "InnoDB",
"field_order": [
"type",
- "label",
- "column_break_4",
"link_to",
+ "column_break_4",
+ "label",
"icon",
"restrict_to_domain",
"section_break_5",
@@ -23,7 +23,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
- "options": "DocType\nReport\nPage",
+ "options": "DocType\nReport\nPage\nDashboard",
"reqd": 1
},
{
@@ -81,13 +81,14 @@
{
"fieldname": "label",
"fieldtype": "Data",
+ "in_list_view": 1,
"label": "Label",
"reqd": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-04-07 19:04:23.645198",
+ "modified": "2020-05-14 16:02:15.420993",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Shortcut",
diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py
index 6d1e865a45..2926a74a55 100644
--- a/frappe/desk/doctype/event/test_event.py
+++ b/frappe/desk/doctype/event/test_event.py
@@ -71,7 +71,7 @@ class TestEvent(unittest.TestCase):
ev = frappe.get_doc(self.test_records[0]).insert()
add({
- "assign_to": "test@example.com",
+ "assign_to": ["test@example.com"],
"doctype": "Event",
"name": ev.name,
"description": "Test Assignment"
@@ -83,7 +83,7 @@ class TestEvent(unittest.TestCase):
# add another one
add({
- "assign_to": self.test_user,
+ "assign_to": [self.test_user],
"doctype": "Event",
"name": ev.name,
"description": "Test Assignment"
@@ -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)
diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.js b/frappe/desk/doctype/list_view_setting/list_view_setting.js
deleted file mode 100644
index 2c70ddf82d..0000000000
--- a/frappe/desk/doctype/list_view_setting/list_view_setting.js
+++ /dev/null
@@ -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) {
-
- // }
-});
diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.json b/frappe/desk/doctype/list_view_setting/list_view_setting.json
deleted file mode 100644
index cd18d3f766..0000000000
--- a/frappe/desk/doctype/list_view_setting/list_view_setting.json
+++ /dev/null
@@ -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
-}
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_settings/__init__.py b/frappe/desk/doctype/list_view_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.js b/frappe/desk/doctype/list_view_settings/list_view_settings.js
new file mode 100644
index 0000000000..db33f71675
--- /dev/null
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.js
@@ -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) {
+
+ // }
+});
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.json b/frappe/desk/doctype/list_view_settings/list_view_settings.json
new file mode 100644
index 0000000000..44761992f1
--- /dev/null
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.json
@@ -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
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py
new file mode 100644
index 0000000000..74e029f499
--- /dev/null
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py
@@ -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
diff --git a/frappe/desk/doctype/list_view_setting/test_list_view_setting.py b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
similarity index 72%
rename from frappe/desk/doctype/list_view_setting/test_list_view_setting.py
rename to frappe/desk/doctype/list_view_settings/test_list_view_settings.py
index 143fc4cce7..c1b2f4a0da 100644
--- a/frappe/desk/doctype/list_view_setting/test_list_view_setting.py
+++ b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
@@ -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
diff --git a/frappe/desk/doctype/module_onboarding/__init__.py b/frappe/desk/doctype/module_onboarding/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/onboarding/onboarding.js b/frappe/desk/doctype/module_onboarding/module_onboarding.js
similarity index 93%
rename from frappe/desk/doctype/onboarding/onboarding.js
rename to frappe/desk/doctype/module_onboarding/module_onboarding.js
index bed7dbd5de..d95920e2ca 100644
--- a/frappe/desk/doctype/onboarding/onboarding.js
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.js
@@ -1,7 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on("Onboarding", {
+frappe.ui.form.on("Module Onboarding", {
refresh: function(frm) {
frappe.boot.developer_mode &&
frm.set_intro(
diff --git a/frappe/desk/doctype/onboarding/onboarding.json b/frappe/desk/doctype/module_onboarding/module_onboarding.json
similarity index 96%
rename from frappe/desk/doctype/onboarding/onboarding.json
rename to frappe/desk/doctype/module_onboarding/module_onboarding.json
index b1d563a9dc..0667ddf6ad 100644
--- a/frappe/desk/doctype/onboarding/onboarding.json
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.json
@@ -90,10 +90,10 @@
}
],
"links": [],
- "modified": "2020-05-01 19:37:21.492405",
+ "modified": "2020-05-18 19:42:39.738869",
"modified_by": "Administrator",
"module": "Desk",
- "name": "Onboarding",
+ "name": "Module Onboarding",
"owner": "Administrator",
"permissions": [
{
@@ -118,6 +118,7 @@
"share": 1
}
],
+ "read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
diff --git a/frappe/desk/doctype/onboarding/onboarding.py b/frappe/desk/doctype/module_onboarding/module_onboarding.py
similarity index 89%
rename from frappe/desk/doctype/onboarding/onboarding.py
rename to frappe/desk/doctype/module_onboarding/module_onboarding.py
index c8527d22b6..89160a60f0 100644
--- a/frappe/desk/doctype/onboarding/onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.py
@@ -8,10 +8,10 @@ from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
-class Onboarding(Document):
+class ModuleOnboarding(Document):
def on_update(self):
if frappe.conf.developer_mode:
- export_to_files(record_list=[['Onboarding', self.name]], record_module=self.module)
+ export_to_files(record_list=[['Module Onboarding', self.name]], record_module=self.module)
for step in self.steps:
export_to_files(record_list=[['Onboarding Step', step.step]], record_module=self.module)
diff --git a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
new file mode 100644
index 0000000000..ef305667b1
--- /dev/null
+++ b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestModuleOnboarding(unittest.TestCase):
+ pass
diff --git a/frappe/desk/doctype/note/note.js b/frappe/desk/doctype/note/note.js
index c237998ccf..5718180b70 100644
--- a/frappe/desk/doctype/note/note.js
+++ b/frappe/desk/doctype/note/note.js
@@ -1,9 +1,9 @@
frappe.ui.form.on("Note", {
refresh: function(frm) {
- if(frm.doc.__islocal) {
+ if (frm.doc.__islocal) {
frm.events.set_editable(frm, true);
} else {
- if(!frm.doc.content) {
+ if (!frm.doc.content) {
frm.doc.content = " ";
}
@@ -18,16 +18,15 @@ frappe.ui.form.on("Note", {
// hide all fields other than content
// no permission
- if(editable && !frm.perm[0].write) return;
+ if (editable && !frm.perm[0].write) return;
// content read_only
- frm.set_df_property("content", "read_only", editable ? 0: 1);
+ frm.set_df_property("content", "read_only", editable ? 0 : 1);
// hide all other fields
$.each(frm.fields_dict, function(fieldname) {
-
- if(fieldname !== "content") {
- frm.set_df_property(fieldname, "hidden", editable ? 0: 1);
+ if (fieldname !== "content") {
+ frm.set_df_property(fieldname, "hidden", editable ? 0 : 1);
}
});
@@ -39,3 +38,16 @@ frappe.ui.form.on("Note", {
frm.is_note_editable = editable;
}
});
+
+frappe.tour['Note'] = [
+ {
+ fieldname: "title",
+ title: "Title of the Note",
+ description: "This is the name by which the note will be saved, you can change this later",
+ },
+ {
+ fieldname: "public",
+ title: "Sets the Note to Public",
+ description: "You can change the visibility of the note with this, setting it to public will allow other users to view it.",
+ },
+];
\ No newline at end of file
diff --git a/frappe/desk/doctype/note/test_note.py b/frappe/desk/doctype/note/test_note.py
index 8d46eaf336..38894a9c3d 100644
--- a/frappe/desk/doctype/note/test_note.py
+++ b/frappe/desk/doctype/note/test_note.py
@@ -20,7 +20,7 @@ class TestNote(unittest.TestCase):
note = self.insert_note()
note.title = 'test note 1'
note.content = '1'
- note.save()
+ note.save(ignore_version=False)
version = frappe.get_doc('Version', dict(docname=note.name))
data = version.get_data()
@@ -33,7 +33,7 @@ class TestNote(unittest.TestCase):
# test add
note.append('seen_by', {'user': 'Administrator'})
- note.save()
+ note.save(ignore_version=False)
version = frappe.get_doc('Version', dict(docname=note.name))
data = version.get_data()
@@ -48,7 +48,7 @@ class TestNote(unittest.TestCase):
# test row change
note.seen_by[0].user = 'Guest'
- note.save()
+ note.save(ignore_version=False)
version = frappe.get_doc('Version', dict(docname=note.name))
data = version.get_data()
@@ -62,7 +62,7 @@ class TestNote(unittest.TestCase):
# test remove
note.seen_by = []
- note.save()
+ note.save(ignore_version=False)
version = frappe.get_doc('Version', dict(docname=note.name))
data = version.get_data()
diff --git a/frappe/desk/doctype/notification_log/notification_log.js b/frappe/desk/doctype/notification_log/notification_log.js
index 654b2b2b06..1f381d115b 100644
--- a/frappe/desk/doctype/notification_log/notification_log.js
+++ b/frappe/desk/doctype/notification_log/notification_log.js
@@ -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(` `);
+ 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(`
+
+ `);
+
+ $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"));
+ }
+ });
}
});
diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json
index ecb746df64..050bf85ead 100644
--- a/frappe/desk/doctype/notification_log/notification_log.json
+++ b/frappe/desk/doctype/notification_log/notification_log.json
@@ -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",
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 17eb6371b1..211b3ae5e6 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -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('', '').replace('
', '')
- 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):
diff --git a/frappe/desk/doctype/notification_log/test_notification_log.py b/frappe/desk/doctype/notification_log/test_notification_log.py
index fe7d56c081..e59aee30c9 100644
--- a/frappe/desk/doctype/notification_log/test_notification_log.py
+++ b/frappe/desk/doctype/notification_log/test_notification_log.py
@@ -13,7 +13,7 @@ class TestNotificationLog(unittest.TestCase):
user = get_user()
assign_task({
- "assign_to": user,
+ "assign_to": [user],
"doctype": 'ToDo',
"name": todo.name,
"description": todo.description
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json
index 6af325507b..85f93e156e 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.json
+++ b/frappe/desk/doctype/notification_settings/notification_settings.json
@@ -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",
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 6b5a13ee27..9b124cd6f4 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -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:
diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index 5fb058d8ce..ec6a1e9190 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "autoname": "CARD.#####",
"creation": "2020-04-15 18:06:39.444683",
"doctype": "DocType",
"editable_grid": 1,
@@ -99,7 +98,7 @@
}
],
"links": [],
- "modified": "2020-05-01 15:23:29.550243",
+ "modified": "2020-05-06 19:47:57.753574",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 2c5655beda..6bb9c7d45c 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -6,10 +6,15 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import cint
+from frappe.model.naming import append_number_if_name_exists
class NumberCard(Document):
- pass
+ def autoname(self):
+ if not self.name:
+ self.name = self.label
+ if frappe.db.exists("Number Card", self.name):
+ self.name = append_number_if_name_exists('Number Card', self.name)
def get_permission_query_conditions(user=None):
if not user:
@@ -65,7 +70,7 @@ def get_result(doc, to_date=None):
if to_date:
filters.append([doc.document_type, 'creation', '<', to_date, False])
- res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters)
+ res = frappe.db.get_list(doc.document_type, fields=fields, filters=filters)
number = res[0]['result'] if res else 0
return cint(number)
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.js b/frappe/desk/doctype/onboarding_step/onboarding_step.js
index 3e5d4d4260..793e044d98 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.js
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.js
@@ -25,6 +25,24 @@ frappe.ui.form.on("Onboarding Step", {
}
},
+ action: function(frm) {
+ if (frm.doc.action == "Show Form Tour") {
+ frm.fields_dict.reference_document.set_description(`You need to add the steps in the contoller JS file. For example: note.js
+
+frappe.tour['Note'] = [
+ {
+ fieldname: "title",
+ title: "Title of the Note",
+ description: "...",
+ }
+];
+
+ `);
+ } else {
+ frm.fields_dict.reference_document.set_description(null);
+ }
+ },
+
disable_form: function(frm) {
frm.set_read_only();
frm.fields
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json
index e1035a4343..365a1c7d21 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.json
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json
@@ -15,10 +15,16 @@
"action",
"column_break_7",
"reference_document",
+ "show_full_form",
+ "is_single",
"reference_report",
"report_reference_doctype",
"report_type",
"report_description",
+ "path",
+ "callback_title",
+ "callback_message",
+ "validate_action",
"field",
"value_to_validate",
"video_url"
@@ -57,7 +63,7 @@
"fieldname": "action",
"fieldtype": "Select",
"label": "Action",
- "options": "Create Entry\nUpdate Settings\nView Report\nWatch Video",
+ "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nWatch Video",
"reqd": 1
},
{
@@ -65,10 +71,11 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\"",
+ "depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"",
"fieldname": "reference_document",
"fieldtype": "Link",
"label": "Reference Document",
+ "mandatory_depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"",
"options": "DocType"
},
{
@@ -83,7 +90,8 @@
"depends_on": "eval:doc.action == \"Watch Video\"",
"fieldname": "video_url",
"fieldtype": "Data",
- "label": "Video URL"
+ "label": "Video URL",
+ "mandatory_depends_on": "eval:doc.action == \"Watch Video\""
},
{
"depends_on": "eval:doc.action == \"View Report\"",
@@ -101,17 +109,19 @@
"label": "Is Skipped"
},
{
- "depends_on": "eval:doc.action == \"Update Settings\"",
+ "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action",
"fieldname": "field",
"fieldtype": "Select",
- "label": "Field"
+ "label": "Field",
+ "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action"
},
{
- "depends_on": "eval:doc.action == \"Update Settings\"",
+ "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action",
"description": "Use % for any non empty value.",
"fieldname": "value_to_validate",
"fieldtype": "Data",
- "label": "Value to Validate"
+ "label": "Value to Validate",
+ "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action"
},
{
"depends_on": "eval:doc.action == \"View Report\"",
@@ -127,10 +137,54 @@
"fieldtype": "Data",
"label": "Report Reference Doctype",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"",
+ "fetch_from": "reference_document.issingle",
+ "fieldname": "is_single",
+ "fieldtype": "Check",
+ "label": "Is Single"
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "description": "Example: #Tree/Account",
+ "fieldname": "path",
+ "fieldtype": "Data",
+ "label": "Path",
+ "mandatory_depends_on": "eval:doc.action == \"Go to Page\""
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "fieldname": "callback_title",
+ "fieldtype": "Data",
+ "label": "Callback Title"
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "description": "This will be shown in a modal after routing",
+ "fieldname": "callback_message",
+ "fieldtype": "Small Text",
+ "label": "Callback Message"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:doc.action == \"Update Settings\"",
+ "fieldname": "validate_action",
+ "fieldtype": "Check",
+ "label": "Validate Field"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.action == \"Create Entry\"",
+ "description": "Show full form instead of a quick entry modal",
+ "fieldname": "show_full_form",
+ "fieldtype": "Check",
+ "label": "Show Full Form?"
}
],
"links": [],
- "modified": "2020-05-04 12:53:19.276952",
+ "modified": "2020-05-18 19:42:30.435604",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Step",
@@ -159,6 +213,7 @@
}
],
"quick_entry": 1,
+ "read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py
index e1cc5dfba4..8086acbb2a 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py
@@ -10,3 +10,7 @@ class OnboardingStep(Document):
def before_export(self, doc):
doc.is_complete = 0
doc.is_skipped = 0
+
+ def validate(self):
+ if self.action == "Go to Page":
+ self.is_mandatory = 0
diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py
index 8e8102d093..804174b56b 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -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)]
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index 76c7caa63d..a916cbca82 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -11,6 +11,7 @@ from frappe.desk.doctype.notification_log.notification_log import enqueue_create
get_title, get_title_html
import frappe.utils
import frappe.share
+import json
class DuplicateToDoError(frappe.ValidationError): pass
@@ -19,17 +20,17 @@ def get(args=None):
if not args:
args = frappe.local.form_dict
- return frappe.get_all('ToDo', fields = ['owner', 'description'], filters = dict(
+ return frappe.get_all('ToDo', fields=['owner', 'name'], filters=dict(
reference_type = args.get('doctype'),
reference_name = args.get('name'),
status = ('!=', 'Cancelled')
- ), limit = 5)
+ ), limit=5)
@frappe.whitelist()
def add(args=None):
"""add in someone's to do list
args = {
- "assign_to": ,
+ "assign_to": [],
"doctype": ,
"name": ,
"description": ,
@@ -40,56 +41,68 @@ def add(args=None):
if not args:
args = frappe.local.form_dict
- if frappe.db.sql("""SELECT `owner`
- FROM `tabToDo`
- WHERE `reference_type`=%(doctype)s
- AND `reference_name`=%(name)s
- AND `status`='Open'
- AND `owner`=%(assign_to)s""", args):
- frappe.throw(_("Already in user's To Do list"), DuplicateToDoError)
- else:
- from frappe.utils import nowdate
+ users_with_duplicate_todo = []
+ shared_with_users = []
- if not args.get('description'):
- args['description'] = _('Assignment for {0} {1}').format(args['doctype'], args['name'])
-
- d = frappe.get_doc({
- "doctype":"ToDo",
- "owner": args['assign_to'],
+ for assign_to in frappe.parse_json(args.get("assign_to")):
+ filters = {
"reference_type": args['doctype'],
"reference_name": args['name'],
- "description": args.get('description'),
- "priority": args.get("priority", "Medium"),
"status": "Open",
- "date": args.get('date', nowdate()),
- "assigned_by": args.get('assigned_by', frappe.session.user),
- 'assignment_rule': args.get('assignment_rule')
- }).insert(ignore_permissions=True)
+ "owner": assign_to
+ }
- # set assigned_to if field exists
- if frappe.get_meta(args['doctype']).get_field("assigned_to"):
- frappe.db.set_value(args['doctype'], args['name'], "assigned_to", args['assign_to'])
+ if frappe.get_all("ToDo", filters=filters):
+ users_with_duplicate_todo.append(assign_to)
+ else:
+ from frappe.utils import nowdate
- doc = frappe.get_doc(args['doctype'], args['name'])
+ if not args.get('description'):
+ args['description'] = _('Assignment for {0} {1}').format(args['doctype'], args['name'])
- # if assignee does not have permissions, share
- if not frappe.has_permission(doc=doc, user=args['assign_to']):
- frappe.share.add(doc.doctype, doc.name, args['assign_to'])
- frappe.msgprint(_('Shared with user {0} with read access').format(args['assign_to']), alert=True)
+ d = frappe.get_doc({
+ "doctype": "ToDo",
+ "owner": assign_to,
+ "reference_type": args['doctype'],
+ "reference_name": args['name'],
+ "description": args.get('description'),
+ "priority": args.get("priority", "Medium"),
+ "status": "Open",
+ "date": args.get('date', nowdate()),
+ "assigned_by": args.get('assigned_by', frappe.session.user),
+ 'assignment_rule': args.get('assignment_rule')
+ }).insert(ignore_permissions=True)
- # make this document followed by assigned user
- follow_document(args['doctype'], args['name'], args['assign_to'])
+ # set assigned_to if field exists
+ if frappe.get_meta(args['doctype']).get_field("assigned_to"):
+ frappe.db.set_value(args['doctype'], args['name'], "assigned_to", assign_to)
- # notify
- notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',\
- description=args.get("description"))
+ doc = frappe.get_doc(args['doctype'], args['name'])
+
+ # if assignee does not have permissions, share
+ if not frappe.has_permission(doc=doc, user=assign_to):
+ frappe.share.add(doc.doctype, doc.name, assign_to)
+ shared_with_users.append(assign_to)
+
+ # make this document followed by assigned user
+ follow_document(args['doctype'], args['name'], assign_to)
+
+ # notify
+ notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',
+ description=args.get("description"))
+
+ if shared_with_users:
+ user_list = format_message_for_assign_to(shared_with_users)
+ frappe.msgprint(_("Shared with the following Users with Read access:{0}").format(user_list, alert=True))
+
+ if users_with_duplicate_todo:
+ user_list = format_message_for_assign_to(users_with_duplicate_todo)
+ frappe.msgprint(_("Already in the following Users ToDo list:{0}").format(user_list, alert=True))
return get(args)
@frappe.whitelist()
def add_multiple(args=None):
- import json
-
if not args:
args = frappe.local.form_dict
@@ -183,3 +196,5 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
enqueue_create_notification(owner, notification_doc)
+def format_message_for_assign_to(users):
+ return " " + " ".join(users)
\ No newline at end of file
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 6c679bf312..72917d0341 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -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,
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index e7f56d313e..1bce14fb2d 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -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))
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 109dd25f4f..4a1302788b 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -212,7 +212,10 @@ def get_notification_config():
def get_filters_for(doctype):
'''get open filters for doctype'''
config = get_notification_config()
- return config.get("for_doctype").get(doctype, {})
+ doctype_config = config.get("for_doctype").get(doctype, {})
+ filters = doctype_config if not isinstance(doctype_config, string_types) else None
+
+ return filters
@frappe.whitelist()
@frappe.read_only()
diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py
index c857bd077f..60e1f3242a 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -6,12 +6,15 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.doctype.global_search_settings.global_search_settings import update_global_search_doctypes
+from frappe.utils.dashboard import sync_dashboards
def install():
update_genders()
update_salutations()
update_global_search_doctypes()
setup_email_linking()
+ sync_dashboards()
+ add_unsubscribe()
@frappe.whitelist()
def update_genders():
@@ -35,4 +38,15 @@ def setup_email_linking():
"email_id": "email_linking@example.com",
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
-
\ No newline at end of file
+
+def add_unsubscribe():
+ email_unsubscribe = [
+ {"email": "admin@example.com", "global_unsubscribe": 1},
+ {"email": "guest@example.com", "global_unsubscribe": 1}
+ ]
+
+ for unsubscribe in email_unsubscribe:
+ if not frappe.get_all("Email Unsubscribe", filters=unsubscribe):
+ doc = frappe.new_doc("Email Unsubscribe")
+ doc.update(unsubscribe)
+ doc.insert(ignore_permissions=True)
diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js
index ff1e906cff..646c31f7a1 100644
--- a/frappe/desk/page/user_profile/user_profile.js
+++ b/frappe/desk/page/user_profile/user_profile.js
@@ -108,24 +108,13 @@ class UserProfile {
});
}
- get_years_since_creation() {
- //Get years since user account created
- this.user_creation = frappe.boot.user.creation;
- let creation_year = this.get_year(this.user_creation);
- let current_year = this.get_year(frappe.datetime.now_date());
- let years_list = [];
- for (var year = current_year; year >= creation_year; year--) {
- years_list.push(year);
- }
- return years_list;
- }
-
- get_year(date_str) {
- return date_str.substring(0, date_str.indexOf('-'));
- }
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',
@@ -201,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];
}
@@ -246,8 +238,8 @@ class UserProfile {
create_heatmap_chart_filters() {
let filters = [
{
- label: this.get_year(frappe.datetime.now_date()),
- options: this.get_years_since_creation(),
+ label: frappe.dashboard_utils.get_year(frappe.datetime.now_date()),
+ options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation),
action: (selected_item) => {
this.update_heatmap_data(frappe.datetime.obj_to_str(selected_item));
}
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 164f6389eb..0edfd57d4f 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -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
@@ -299,7 +324,6 @@ def export_query():
_("You can try changing the filters of your report."))
return
- data.columns = [col for col in data.columns if isinstance(col, dict) and not col.get('hidden')]
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
@@ -316,7 +340,8 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation):
# add column headings
for idx in range(len(data.columns)):
- result[0].append(columns[idx]["label"])
+ if not columns[idx].get("hidden"):
+ result[0].append(columns[idx]["label"])
# build table from result
for i, row in enumerate(data.result):
diff --git a/frappe/email/doctype/document_follow/test_document_follow.py b/frappe/email/doctype/document_follow/test_document_follow.py
index 4f1a8733cc..1208a6c5c1 100644
--- a/frappe/email/doctype/document_follow/test_document_follow.py
+++ b/frappe/email/doctype/document_follow/test_document_follow.py
@@ -14,7 +14,7 @@ class TestDocumentFollow(unittest.TestCase):
event_doc = get_event()
event_doc.description = "This is a test description for sending mail"
- event_doc.save()
+ event_doc.save(ignore_version=False)
doc = document_follow.follow_document("Event", event_doc.name , user.name, force=True)
self.assertEquals(doc.user, user.name)
@@ -45,12 +45,12 @@ def get_event():
return doc
def get_user():
- doc = frappe.new_doc("User")
- doc.email = "test@docsub.com"
- doc.first_name = "Test"
- doc.last_name = "User"
- doc.send_welcome_email = 0
- doc.document_follow_notify = 1
- doc.document_follow_frequency = "Hourly"
- doc.insert()
- return doc
\ No newline at end of file
+ doc = frappe.new_doc("User")
+ doc.email = "test@docsub.com"
+ doc.first_name = "Test"
+ doc.last_name = "User"
+ doc.send_welcome_email = 0
+ doc.document_follow_notify = 1
+ doc.document_follow_frequency = "Hourly"
+ doc.insert()
+ return doc
\ No newline at end of file
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index 6bde0291a0..057638697a 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -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
-}
\ No newline at end of file
+}
diff --git a/frappe/email/doctype/newsletter/newsletter..json b/frappe/email/doctype/newsletter/newsletter..json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index 719d51c176..01f75be954 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -17,7 +17,7 @@
"subject",
"message",
"send_unsubscribe_link",
- "send_attachements",
+ "send_attachments",
"published",
"route",
"test_the_newsletter",
@@ -73,12 +73,6 @@
"fieldtype": "Check",
"label": "Send Unsubscribe Link"
},
- {
- "default": "0",
- "fieldname": "send_attachements",
- "fieldtype": "Check",
- "label": "Send Attachements"
- },
{
"default": "0",
"fieldname": "published",
@@ -127,6 +121,12 @@
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_attachments",
+ "fieldtype": "Check",
+ "label": "Send Attachments"
}
],
"has_web_view": 1,
@@ -135,7 +135,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2020-03-02 06:26:51.622521",
+ "modified": "2020-05-12 18:09:40.137138",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 2469569892..2dccfbead4 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -67,7 +67,7 @@ class Newsletter(WebsiteGenerator):
frappe.db.auto_commit_on_many_writes = True
attachments = []
- if self.send_attachements:
+ if self.send_attachments:
files = frappe.get_all("File", fields=["name"], filters={"attached_to_doctype": "Newsletter",
"attached_to_name": self.name}, order_by="creation desc")
diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js
index 44056955f7..02fc8512ca 100644
--- a/frappe/email/doctype/notification/notification.js
+++ b/frappe/email/doctype/notification/notification.js
@@ -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');
diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json
index 14eff2251a..d1526f5fe4 100644
--- a/frappe/email/doctype/notification/notification.json
+++ b/frappe/email/doctype/notification/notification.json
@@ -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",
@@ -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": "Condition Examples:
\ndoc.status==\"Open\" doc.due_date==nowdate() doc.total > 40000\n \n"
+ "options": "Condition Examples:
\ndoc.status==\"Open\" doc.due_date==nowdate() doc.total > 40000\n \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": "Message Example \n\n<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 "
+ "options": "Message Example \n\n<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 ",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.channel=='Slack'",
"fieldname": "slack_message_examples",
"fieldtype": "HTML",
"label": "Message Examples",
- "options": "Message Example \n\n*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n\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 "
+ "options": "Message Example \n\n*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n\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 ",
+ "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",
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 8c011ade65..8e53b50fa2 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -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 [{
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index f44c6e775a..43c4bb8333 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -75,22 +75,6 @@ This is the text version of this email
else:
self.assertTrue(True)
- def test_rfc_5322_header_is_wrapped_at_998_chars(self):
- # unfortunately the db can only hold 140 chars so this can't be tested properly. test at max chars anyway.
- email = get_email_queue(
- recipients=['test@example.com'],
- sender='me@example.com',
- subject='Test Subject',
- content='Whatever ',
- text_content='whatever',
- message_id="a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
- ".really.long.message.id.that.should.not.wrap.unti")
- result = safe_decode(prepare_message(email=email, recipient='test@test.com',
- recipients_list=[]))
- self.assertTrue(
- "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
- ".really.long.message.id.that.should.not.wrap.unti" in result)
-
def test_image(self):
img_signature = '''
Content-Type: image/png
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 9a1c1fb0b0..1aac339228 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -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
@@ -49,6 +54,11 @@ class Redirect(Exception):
class CSRFTokenError(Exception):
http_status_code = 400
+
+class TooManyRequestsError(Exception):
+ http_status_code = 429
+
+
class ImproperDBConfigurationError(Exception):
"""
Used when frappe detects that database or tables are not properly
diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py
index 8611c21720..919c334e51 100644
--- a/frappe/frappeclient.py
+++ b/frappe/frappeclient.py
@@ -15,6 +15,9 @@ class AuthError(Exception):
class SiteExpiredError(Exception):
pass
+class SiteUnreachableError(Exception):
+ pass
+
class FrappeException(Exception):
pass
@@ -53,9 +56,16 @@ class FrappeClient(object):
if r.status_code==200 and r.json().get('message') in ("Logged In", "No App"):
return r.json()
+ elif r.status_code == 502:
+ raise SiteUnreachableError
else:
- if json.loads(r.text).get('exc_type') == "SiteExpiredError":
- raise SiteExpiredError
+ try:
+ error = json.loads(r.text)
+ if error.get('exc_type') == "SiteExpiredError":
+ raise SiteExpiredError
+ except json.decoder.JSONDecodeError:
+ error = r.text
+ print(error)
raise AuthError
def setup_key_authentication_headers(self):
diff --git a/frappe/installer.py b/frappe/installer.py
index 54402f0087..4fc19b282a 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -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
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
index 2a036f4838..4b595b1abf 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
@@ -90,7 +90,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:
diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py
index 0e28c1306c..6455623281 100644
--- a/frappe/integrations/doctype/google_contacts/google_contacts.py
+++ b/frappe/integrations/doctype/google_contacts/google_contacts.py
@@ -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))
@@ -218,7 +221,7 @@ def insert_contacts_to_google_contacts(doc, method=None):
emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids]
try:
- contact = google_contacts.people().createContact(parent='people/me', body={"names": [names],"phoneNumbers": phoneNumbers,
+ contact = google_contacts.people().createContact(body={"names": [names],"phoneNumbers": phoneNumbers,
"emailAddresses": emailAddresses}).execute()
frappe.db.set_value("Contact", doc.name, "google_contacts_id", contact.get("resourceName"))
except HttpError as err:
diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
index 5e464d4882..1d2f7f9495 100644
--- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
+++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
@@ -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.
diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py
new file mode 100644
index 0000000000..887e191e16
--- /dev/null
+++ b/frappe/integrations/frappe_providers/__init__.py
@@ -0,0 +1,13 @@
+# imports - standard imports
+import sys
+
+# imports - module imports
+from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrator
+
+
+def migrate_to(local_site, frappe_provider):
+ if frappe_provider in ("frappe.cloud", "frappecloud.com"):
+ return frappecloud_migrator(local_site, frappe_provider)
+ else:
+ print("{} is not supported yet".format(frappe_provider))
+ sys.exit(1)
diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py
new file mode 100644
index 0000000000..291b8af647
--- /dev/null
+++ b/frappe/integrations/frappe_providers/frappecloud.py
@@ -0,0 +1,402 @@
+# imports - standard imports
+import getpass
+import json
+import re
+import sys
+
+# imports - third party imports
+import click
+from html2text import html2text
+import requests
+
+# imports - module imports
+import frappe
+import frappe.utils.backups
+from frappe.utils import get_installed_apps_info
+from frappe.utils.commands import render_table, add_line_after, add_line_before
+
+
+# TODO: check upgrade compatibility
+
+
+def render_actions_table():
+ actions_table = [["#", "Action"]]
+ actions = []
+
+ for n, action in enumerate(migrator_actions):
+ actions_table.append([n+1, action["title"]])
+ actions.append(action["fn"])
+
+ render_table(actions_table)
+ return actions
+
+
+def render_site_table(sites_info):
+ sites_table = [["#", "Site Name", "Status"]]
+ available_sites = []
+
+ for n, site_data in enumerate(sites_info):
+ name, status = site_data["name"], site_data["status"]
+ if status in ("Active", "Broken"):
+ sites_table.append([n + 1, name, status])
+ available_sites.append(name)
+
+ render_table(sites_table)
+ return available_sites
+
+
+def render_teams_table(teams):
+ teams_table = [["#", "Team"]]
+
+ for n, team in enumerate(teams):
+ teams_table.append([n+1, team])
+
+ render_table(teams_table)
+
+
+def render_plan_table(plans_list):
+ plans_table = [["Plan", "CPU Time"]]
+ visible_headers = ["name", "cpu_time_per_day"]
+
+ for plan in plans_list:
+ plan, cpu_time = [plan[header] for header in visible_headers]
+ plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
+
+ render_table(plans_table)
+
+
+def render_group_table(app_groups):
+ # title row
+ app_groups_table = [["#", "App Group", "Apps"]]
+
+ # all rows
+ for idx, app_group in enumerate(app_groups):
+ apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
+ row = [idx + 1, app_group["name"], apps_list]
+ app_groups_table.append(row)
+
+ render_table(app_groups_table)
+
+
+def handle_request_failure(request=None, message=None, traceback=True, exit_code=1):
+ message = message or "Request failed with error code {}".format(request.status_code)
+ response = html2text(request.text) if traceback else ""
+
+ print("{0}{1}".format(message, "\n" + response))
+ sys.exit(exit_code)
+
+
+@add_line_after
+def select_primary_action():
+ actions = render_actions_table()
+ idx = click.prompt("What do you want to do?", type=click.IntRange(1, len(actions))) - 1
+
+ return actions[idx]
+
+
+@add_line_after
+def select_site():
+ get_all_sites_request = session.post(all_site_url, headers={
+ "accept": "application/json",
+ "accept-encoding": "gzip, deflate, br",
+ "content-type": "application/json; charset=utf-8"
+ })
+
+ if get_all_sites_request.ok:
+ all_sites = get_all_sites_request.json()["message"]
+ available_sites = render_site_table(all_sites)
+
+ while True:
+ selected_site = click.prompt("Name of the site you want to restore to", type=str).strip()
+ if selected_site in available_sites:
+ return selected_site
+ else:
+ print("Site {} does not exist. Try again ❌".format(selected_site))
+ else:
+ print("Couldn't retrive sites list...Try again later")
+ sys.exit(1)
+
+
+@add_line_before
+def select_team(session):
+ # get team options
+ account_details_sc = session.post(account_details_url)
+ if account_details_sc.ok:
+ account_details = account_details_sc.json()["message"]
+ available_teams = account_details["teams"]
+
+ # ask if they want to select, go ahead with if only one exists
+ if len(available_teams) == 1:
+ team = available_teams[0]
+ else:
+ render_teams_table(available_teams)
+ idx = click.prompt("Select Team", type=click.IntRange(1, len(available_teams))) - 1
+ team = available_teams[idx]
+
+ print("Team '{}' set for current session".format(team))
+
+ return team
+
+
+def get_new_site_options():
+ site_options_sc = session.post(options_url)
+
+ if site_options_sc.ok:
+ site_options = site_options_sc.json()["message"]
+ return site_options
+ else:
+ print("Couldn't retrive New site information: {}".format(site_options_sc.status_code))
+
+
+def is_valid_subdomain(subdomain):
+ if len(subdomain) < 5:
+ print("Subdomain too short. Use 5 or more characters")
+ return False
+ matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain)
+ if matched:
+ return True
+ print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens")
+
+
+def is_subdomain_available(subdomain):
+ res = session.post(site_exists_url, {"subdomain": subdomain})
+ if res.ok:
+ available = not res.json()["message"]
+ if not available:
+ print("Subdomain already exists! Try another one")
+
+ return available
+
+
+@add_line_after
+def choose_plan(plans_list):
+ print("{} plans available".format(len(plans_list)))
+ available_plans = [plan["name"] for plan in plans_list]
+ render_plan_table(plans_list)
+
+ while True:
+ input_plan = click.prompt("Select Plan").strip()
+ if input_plan in available_plans:
+ print("{} Plan selected ✅".format(input_plan))
+ return input_plan
+ else:
+ print("Invalid Selection ❌")
+
+
+@add_line_after
+def check_app_compat(available_group):
+ is_compat = True
+ incompatible_apps, filtered_apps, branch_msgs = [], [], []
+ existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()]
+ print("Checking availability of existing app group")
+
+ for (app, branch) in existing_group:
+ info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ]
+ if info:
+ app_title, available_branch = info[0]
+
+ if branch != available_branch:
+ print("⚠️ App {}:{} => {}".format(app, branch, available_branch))
+ branch_msgs.append([app, branch, available_branch])
+ filtered_apps.append(app_title)
+ is_compat = False
+
+ else:
+ print("✅ App {}:{}".format(app, branch))
+ filtered_apps.append(app_title)
+
+ else:
+ incompatible_apps.append(app)
+ print("❌ App {}:{}".format(app, branch))
+ is_compat = False
+
+ start_msg = "\nSelecting this group will "
+ incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else ""
+ branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else ""
+ changes = (incompatible_apps + branch_change) or "be perfect for you :)"
+ warning_message = start_msg + changes
+ print(warning_message)
+
+ return is_compat, filtered_apps
+
+
+@add_line_after
+def filter_apps(app_groups):
+ render_group_table(app_groups)
+
+ while True:
+ app_group_index = click.prompt("Select App Group Number", type=int) - 1
+ try:
+ if app_group_index == -1:
+ raise IndexError
+ selected_group = app_groups[app_group_index]
+ except IndexError:
+ print("Invalid Selection ❌")
+ continue
+
+ is_compat, filtered_apps = check_app_compat(selected_group)
+
+ if is_compat or click.confirm("Continue anyway?"):
+ print("App Group {} selected! ✅".format(selected_group["name"]))
+ break
+
+ return selected_group["name"], filtered_apps
+
+
+@add_line_after
+def get_subdomain(domain):
+ while True:
+ subdomain = click.prompt("Enter subdomain").strip()
+ if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain):
+ print("Site Domain: {}.{}".format(subdomain, domain))
+ return subdomain
+
+
+@add_line_after
+def upload_backup(local_site):
+ # take backup
+ files_session = {}
+ print("Taking backup for site {}".format(local_site))
+ odb = frappe.utils.backups.new_backup(ignore_files=False, force=True)
+
+ # upload files
+ for x, (file_type, file_path) in enumerate([
+ ("database", odb.backup_path_db),
+ ("public", odb.backup_path_files),
+ ("private", odb.backup_path_private_files)
+ ]):
+ file_upload_response = session.post(files_url, data={}, files={
+ "file": open(file_path, "rb"),
+ "is_private": 1,
+ "folder": "Home",
+ "method": "press.api.site.upload_backup",
+ "type": file_type
+ })
+ print("Uploading files ({}/3)".format(x+1), end="\r")
+ if file_upload_response.ok:
+ files_session[file_type] = file_upload_response.json()["message"]
+ else:
+ print("Upload failed for: {}".format(file_path))
+
+ files_uploaded = { k: v["file_url"] for k, v in files_session.items() }
+ print("Uploaded backup files! ✅")
+
+ return files_uploaded
+
+
+def new_site(local_site):
+ # get new site options
+ site_options = get_new_site_options()
+
+ # set preferences from site options
+ subdomain = get_subdomain(site_options["domain"])
+ plan = choose_plan(site_options["plans"])
+
+ app_groups = site_options["groups"]
+ selected_group, filtered_apps = filter_apps(app_groups)
+ files_uploaded = upload_backup(local_site)
+
+ # push to frappe_cloud
+ payload = json.dumps({
+ "site": {
+ "apps": filtered_apps,
+ "files": files_uploaded,
+ "group": selected_group,
+ "name": subdomain,
+ "plan": plan
+ }
+ })
+
+ session.headers.update({"Content-Type": "application/json; charset=utf-8"})
+ site_creation_request = session.post(upload_url, payload)
+
+ if site_creation_request.ok:
+ site_url = site_creation_request.json()["message"]
+ print("Your site {} is being migrated ✨".format(local_site))
+ print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
+ print("Your site URL: {}".format(site_url))
+ else:
+ handle_request_failure(site_creation_request)
+
+
+def restore_site(local_site):
+ # get list of existing sites they can restore
+ selected_site = select_site()
+
+ # TODO: check if they can restore it
+
+ click.confirm("This is an irreversible action. Are you sure you want to continue?", abort=True)
+
+ # backup site
+ files_uploaded = upload_backup(local_site)
+
+ # push to frappe_cloud
+ payload = json.dumps({
+ "name": selected_site,
+ "files": files_uploaded
+ })
+ headers = {"Content-Type": "application/json; charset=utf-8"}
+ site_restore_request = session.post(restore_site_url, payload, headers=headers)
+
+ if site_restore_request.ok:
+ print("Your site {0} is being restored on {1} ✨".format(local_site, selected_site))
+ print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, selected_site))
+ print("Your site URL: {}".format(selected_site))
+ else:
+ handle_request_failure(site_restore_request)
+
+
+@add_line_after
+def create_session():
+ print("Frappe Cloud credentials @ {}".format(remote_site))
+
+ # take user input from STDIN
+ username = click.prompt("Username").strip()
+ password = getpass.unix_getpass()
+
+ auth_credentials = {"usr": username, "pwd": password}
+
+ session = requests.Session()
+ login_sc = session.post(login_url, auth_credentials)
+
+ if login_sc.ok:
+ print("Authorization Successful! ✅")
+ team = select_team(session)
+ session.headers.update({"X-Press-Team": team })
+ 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)
+
+ 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()
+
+ # available actions defined in migrator_actions
+ primary_action = select_primary_action()
+
+ frappe.init(site=local_site)
+ frappe.connect()
+
+ primary_action(local_site)
+
+ frappe.destroy()
diff --git a/frappe/integrations/offsite_backup_utils.py b/frappe/integrations/offsite_backup_utils.py
index c280a1d9dd..7e80cb68c4 100644
--- a/frappe/integrations/offsite_backup_utils.py
+++ b/frappe/integrations/offsite_backup_utils.py
@@ -10,7 +10,7 @@ from frappe.utils import split_emails, get_backups_path
def send_email(success, service_name, doctype, email_field, error_status=None):
- recipients = get_recipients(service_name, email_field)
+ recipients = get_recipients(doctype, email_field)
if not recipients:
frappe.log_error("No Email Recipient found for {0}".format(service_name),
"{0}: Failed to send backup status email".format(service_name))
@@ -36,11 +36,11 @@ def send_email(success, service_name, doctype, email_field, error_status=None):
frappe.sendmail(recipients=recipients, subject=subject, message=message)
-def get_recipients(service_name, email_field):
+def get_recipients(doctype, email_field):
if not frappe.db:
frappe.connect()
- return split_emails(frappe.db.get_value(service_name, None, email_field))
+ return split_emails(frappe.db.get_value(doctype, None, email_field))
def get_latest_backup_file(with_files=False):
diff --git a/frappe/migrate.py b/frappe/migrate.py
index 043b6817d7..9ec23d8ae7 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -5,11 +5,14 @@ from __future__ import unicode_literals
import json
import os
+import sys
import frappe
import frappe.translate
import frappe.modules.patch_handler
import frappe.model.sync
from frappe.utils.fixtures import sync_fixtures
+from frappe.utils.connections import check_connection
+from frappe.utils.dashboard import sync_dashboards
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
from frappe.website import render
@@ -18,11 +21,13 @@ from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.utils import global_search
+
def migrate(verbose=True, rebuild_website=False, skip_failing=False):
'''Migrate all apps to the latest version, will:
- run before migrate hooks
- run patches
- sync doctypes (schema)
+ - sync dashboards
- sync fixtures
- sync desktop icons
- sync web pages (from /www)
@@ -30,6 +35,19 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
- run after migrate hooks
'''
+ service_status = check_connection(redis_services=["redis_cache"])
+ if False in service_status.values():
+ for service in service_status:
+ if not service_status.get(service, True):
+ print("{} service is not running.".format(service))
+ print("""Cannot run bench migrate without the services running.
+If you are running bench in development mode, make sure that bench is running:
+
+$ bench start
+
+Otherwise, check the server logs and ensure that all the required services are running.""")
+ sys.exit(1)
+
touched_tables_file = frappe.get_site_path('touched_tables.json')
if os.path.exists(touched_tables_file):
os.remove(touched_tables_file)
@@ -53,6 +71,7 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
frappe.translate.clear_cache()
sync_jobs()
sync_fixtures()
+ sync_dashboards()
sync_customizations()
sync_languages()
@@ -64,6 +83,9 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
# add static pages to global search
global_search.update_global_search_for_all_web_pages()
+ # updating installed applications data
+ frappe.get_single('Installed Applications').update_versions()
+
#run after_migrate hooks
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('after_migrate', app_name=app):
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index 93ef78df7b..3c5d996439 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -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',
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index feeb96898a..106d21eb51 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -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")
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 596aa18b09..19517aa4a1 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -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
\ No newline at end of file
+ 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)
\ No newline at end of file
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 65cb6073b7..843cb421fe 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -297,8 +297,7 @@ class Document(BaseDocument):
if ignore_permissions!=None:
self.flags.ignore_permissions = ignore_permissions
- if ignore_version!=None:
- self.flags.ignore_version = ignore_version
+ self.flags.ignore_version = frappe.flags.in_test if ignore_version is None else ignore_version
if self.get("__islocal") or not self.get("name"):
self.insert()
@@ -1339,4 +1338,4 @@ def check_doctype_has_consumers(doctype):
if len(event_consumers) and event_consumers[0]:
return True
- return False
\ No newline at end of file
+ return False
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 2321e0c22a..0c5ec75597 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -437,39 +437,47 @@ 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
def add_doctype_links(self, data):
'''add `links` child table in standard link dashboard format'''
+ dashboard_links = []
+
if hasattr(self, 'links') and self.links:
- if not data.transactions:
- # init groups
- data.transactions = []
- data.non_standard_fieldnames = {}
+ dashboard_links.extend(self.links)
- for link in self.links:
- link.added = False
- for group in data.transactions:
- # group found
- if group.label == link.label:
- if not link.link_doctype in group.items:
- group.items.append(link.link_doctype)
- link.added = True
+ if frappe.get_all("Custom Link", {"document_type": self.name}):
+ dashboard_links.extend(frappe.get_doc("Custom Link", self.name).links)
- if not link.added:
- # group not found, make a new group
- data.transactions.append(dict(
- label = link.group,
- items = [link.link_doctype]
- ))
+ if not data.transactions:
+ # init groups
+ data.transactions = []
+ data.non_standard_fieldnames = {}
- if link.link_fieldname != data.fieldname:
- if data.fieldname:
- data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
- else:
- data.fieldname = link.link_fieldname
+ for link in dashboard_links:
+ link.added = False
+ for group in data.transactions:
+ group = frappe._dict(group)
+ # group found
+ if link.group and group.label == link.group:
+ if link.link_doctype not in group.get('items'):
+ group.get('items').append(link.link_doctype)
+ link.added = True
+
+ if not link.added:
+ # group not found, make a new group
+ data.transactions.append(dict(
+ label = link.group,
+ items = [link.link_doctype]
+ ))
+
+ if link.link_fieldname != data.fieldname:
+ if data.fieldname:
+ data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
+ else:
+ data.fieldname = link.link_fieldname
def get_row_template(self):
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 4491a352bc..1e3f127b99 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -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):
'''
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index c2acb59f63..b7d9d4d548 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -45,10 +45,13 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
("data_migration", "data_migration_mapping"),
("data_migration", "data_migration_plan_mapping"),
("data_migration", "data_migration_plan"),
+ ("desk", "number_card"),
+ ("desk", "dashboard_chart"),
+ ("desk", "dashboard"),
("desk", "onboarding_permission"),
("desk", "onboarding_step"),
("desk", "onboarding_step_map"),
- ("desk", "onboarding"),
+ ("desk", "module_onboarding"),
("desk", "desk_card"),
("desk", "desk_chart"),
("desk", "desk_shortcut"),
@@ -82,7 +85,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
'data_migration_mapping', 'data_migration_plan', 'desk_page',
- 'onboarding_step', 'onboarding']
+ 'onboarding_step', 'module_onboarding']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 4384e7c8f5..ea563dfc13 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -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'
}
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index cddef4f910..27649b8da9 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -13,7 +13,7 @@ ignore_values = {
"Print Format": ["disabled"],
"Notification": ["enabled"],
"Print Style": ["disabled"],
- "Onboarding": ['is_complete'],
+ "Module Onboarding": ['is_complete'],
"Onboarding Step": ['is_complete', 'is_skipped']
}
diff --git a/frappe/monitor.py b/frappe/monitor.py
index b056286cf9..6802a59584 100644
--- a/frappe/monitor.py
+++ b/frappe/monitor.py
@@ -81,6 +81,12 @@ class Monitor:
self.data.request.status_code = response.status_code
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
+ if hasattr(frappe.local, "rate_limiter"):
+ limiter = frappe.local.rate_limiter
+ self.data.request.counter = limiter.counter
+ if limiter.rejected:
+ self.data.request.reset = limiter.reset
+
self.store()
except Exception:
traceback.print_exc()
diff --git a/frappe/patches.txt b/frappe/patches.txt
index a086fa6f4a..fb5bf447b7 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -278,3 +278,13 @@ 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
diff --git a/frappe/patches/v13_0/email_unsubscribe.py b/frappe/patches/v13_0/email_unsubscribe.py
new file mode 100644
index 0000000000..69ed1be948
--- /dev/null
+++ b/frappe/patches/v13_0/email_unsubscribe.py
@@ -0,0 +1,13 @@
+import frappe
+
+def execute():
+ email_unsubscribe = [
+ {"email": "admin@example.com", "global_unsubscribe": 1},
+ {"email": "guest@example.com", "global_unsubscribe": 1}
+ ]
+
+ for unsubscribe in email_unsubscribe:
+ if not frappe.get_all("Email Unsubscribe", filters=unsubscribe):
+ doc = frappe.new_doc("Email Unsubscribe")
+ doc.update(unsubscribe)
+ doc.insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/frappe/patches/v13_0/remove_tailwind_from_page_builder.py b/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
new file mode 100644
index 0000000000..6e7bf67bac
--- /dev/null
+++ b/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
@@ -0,0 +1,13 @@
+# 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("website", "doctype", "web_page_block")
+ # remove unused templates
+ frappe.delete_doc("Web Template", "Navbar with Links on Right", force=1)
+ frappe.delete_doc("Web Template", "Footer Horizontal", force=1)
+
diff --git a/frappe/patches/v13_0/rename_onboarding.py b/frappe/patches/v13_0/rename_onboarding.py
new file mode 100644
index 0000000000..c506c6076e
--- /dev/null
+++ b/frappe/patches/v13_0/rename_onboarding.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ if frappe.db.exists("DocType", "Onboarding"):
+ frappe.rename_doc("DocType", "Onboarding", "Module Onboarding", ignore_if_exists=True)
+
diff --git a/frappe/patches/v13_0/set_unique_for_page_view.py b/frappe/patches/v13_0/set_unique_for_page_view.py
new file mode 100644
index 0000000000..2a084e52e3
--- /dev/null
+++ b/frappe/patches/v13_0/set_unique_for_page_view.py
@@ -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))
diff --git a/frappe/patches/v13_0/site_wise_logging.py b/frappe/patches/v13_0/site_wise_logging.py
new file mode 100644
index 0000000000..6f04e0c9dd
--- /dev/null
+++ b/frappe/patches/v13_0/site_wise_logging.py
@@ -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)
\ No newline at end of file
diff --git a/frappe/patches/v13_0/update_date_filters_in_user_settings.py b/frappe/patches/v13_0/update_date_filters_in_user_settings.py
new file mode 100644
index 0000000000..d4c6aa1d03
--- /dev/null
+++ b/frappe/patches/v13_0/update_date_filters_in_user_settings.py
@@ -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)
+
+
diff --git a/frappe/public/build.json b/frappe/public/build.json
index d56907b558..997a3092ad 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -1,7 +1,4 @@
{
- "css/tailwind.css": [
- "public/tailwind.css"
- ],
"css/frappe-web-b4.css": [
"public/scss/website.scss",
"public/less/indicator.less"
@@ -112,7 +109,9 @@
"public/less/chat.less",
"public/less/filters.less",
"public/less/social.less",
- "node_modules/frappe-charts/dist/frappe-charts.min.css"
+ "node_modules/frappe-charts/dist/frappe-charts.min.css",
+ "node_modules/driver.js/dist/driver.min.css",
+ "public/less/driver.less"
],
"css/frappe-rtl.css": [
"public/css/bootstrap-rtl.css",
@@ -244,12 +243,15 @@
"public/js/frappe/utils/energy_point_utils.js",
"public/js/frappe/utils/dashboard_utils.js",
"public/js/frappe/ui/chart.js",
+ "public/js/frappe/ui/driver.js",
"public/js/frappe/barcode_scanner/index.js"
],
"css/form.min.css": [
"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",
diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css
index 92ac433fd2..40c6149927 100644
--- a/frappe/public/css/email.css
+++ b/frappe/public/css/email.css
@@ -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 */
diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js
index f54b9e5cbe..6b723d508c 100644
--- a/frappe/public/js/frappe/chat.js
+++ b/frappe/public/js/frappe/chat.js
@@ -2259,14 +2259,19 @@ class extends Component {
) : null,
h("div","",
h("div", { class: "panel-title" },
- h("div", { class: "cursor-pointer", onclick: () => { frappe.set_route(item.route) }},
+ h("div", { class: "cursor-pointer", onclick: () => {
+ frappe.session.user !== "Guest" ?
+ frappe.set_route(item.route) : null;
+ }},
h(frappe.Chat.Widget.MediaProfile, { ...item })
)
)
),
- h("div", { class: popper ? "col-xs-1" : "col-xs-3" },
+ h("div", { class: popper ? "col-xs-2" : "col-xs-3" },
h("div", { class: "text-right" },
-
+ frappe._.is_mobile() && h(frappe.components.Button, { class: "frappe-chat-close", onclick: props.toggle },
+ h(frappe.components.Octicon, { type: "x" })
+ )
)
)
)
diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js
index 1900a1f789..27d81b75b7 100644
--- a/frappe/public/js/frappe/data_import/import_preview.js
+++ b/frappe/public/js/frappe/data_import/import_preview.js
@@ -81,7 +81,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
`;
return {
id: frappe.utils.get_random(6),
- name: col.header_title || df.label,
+ name: col.header_title || (df ? df.label : 'Untitled Column'),
content: column_title,
skip_import: true,
editable: false,
diff --git a/frappe/public/js/frappe/form/controls/control.js b/frappe/public/js/frappe/form/controls/control.js
index 2bf6292abc..168da2717c 100644
--- a/frappe/public/js/frappe/form/controls/control.js
+++ b/frappe/public/js/frappe/form/controls/control.js
@@ -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, "");
diff --git a/frappe/public/js/frappe/form/controls/duration.js b/frappe/public/js/frappe/form/controls/duration.js
new file mode 100644
index 0000000000..58df8e15e6
--- /dev/null
+++ b/frappe/public/js/frappe/form/controls/duration.js
@@ -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 = $(
+ ``
+ );
+ this.$wrapper.append(this.$picker);
+ this.build_numeric_input("days", !this.duration_options.show_days);
+ this.build_numeric_input("hours", false);
+ this.build_numeric_input("minutes", false);
+ this.build_numeric_input("seconds", !this.duration_options.show_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 = $(`
+
+ `);
+
+ let $input = $(`
`).prepend($duration_input);
+
+ if (max) {
+ $duration_input.attr("max", max);
+ }
+
+ this.inputs[label] = $duration_input;
+
+ let $control = $(`
+ `
+ );
+
+ 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.show_days) {
+ total_duration.days = parseInt(this.inputs.days.val());
+ }
+ if (this.duration_options.show_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;
+ }
+});
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/controls/multiselect_pills.js b/frappe/public/js/frappe/form/controls/multiselect_pills.js
index 8796c95eaa..6190204357 100644
--- a/frappe/public/js/frappe/form/controls/multiselect_pills.js
+++ b/frappe/public/js/frappe/form/controls/multiselect_pills.js
@@ -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);
diff --git a/frappe/public/js/frappe/form/controls/rating.js b/frappe/public/js/frappe/form/controls/rating.js
index 9a68cec2be..34e890d45c 100644
--- a/frappe/public/js/frappe/form/controls/rating.js
+++ b/frappe/public/js/frappe/form/controls/rating.js
@@ -16,7 +16,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({
$(this.input_area).find('i').hover((ev) => {
const el = $(ev.currentTarget);
let star_value = el.data('rating');
- el.parent().children('i.fa').each( function(e){
+ el.parent().children('i.fa').each( function(e) {
if (e < star_value) {
$(this).addClass('star-hover');
} else {
diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js
index beec168dfd..bb44408c2a 100644
--- a/frappe/public/js/frappe/form/footer/timeline.js
+++ b/frappe/public/js/frappe/form/footer/timeline.js
@@ -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;
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index a5853d96f5..369e4a56d4 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -464,9 +464,9 @@ frappe.ui.form.Form = class FrappeForm {
}
run_after_load_hook() {
- if (frappe.route_options.after_load) {
- let route_callback = frappe.route_options.after_load;
- delete frappe.route_options.after_load;
+ if (frappe.route_hooks.after_load) {
+ let route_callback = frappe.route_hooks.after_load;
+ delete frappe.route_hooks.after_load;
route_callback(this);
}
@@ -580,9 +580,9 @@ frappe.ui.form.Form = class FrappeForm {
me.script_manager.trigger("after_save");
- if (frappe.route_options.after_save) {
- let route_callback = frappe.route_options.after_save;
- delete frappe.route_options.after_save;
+ if (frappe.route_hooks.after_save) {
+ let route_callback = frappe.route_hooks.after_save;
+ delete frappe.route_hooks.after_save;
route_callback(me);
}
@@ -650,7 +650,14 @@ frappe.ui.form.Form = class FrappeForm {
frappe.utils.play_sound("submit");
callback && callback();
me.script_manager.trigger("on_submit")
- .then(() => resolve(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);
});
@@ -1556,6 +1563,41 @@ frappe.ui.form.Form = class FrappeForm {
$el.find('input, select, textarea').focus();
}, 1000);
}
+
+ show_tour(on_finish) {
+ if (!Array.isArray(frappe.tour[this.doctype])) {
+ return;
+ }
+
+ const driver = new frappe.Driver({
+ overlayClickNext: true,
+ keyboardControl: true,
+ nextBtnText: 'Next',
+ prevBtnText: 'Previous',
+ opacity: 0.25,
+ onNext: () => {
+ if (!driver.hasNextStep()) {
+ on_finish && on_finish();
+ }
+ }
+ });
+
+ this.layout.sections.forEach(section => section.collapse(false));
+
+ let steps = frappe.tour[this.doctype].map(step => {
+ let field = this.get_docfield(step.fieldname);
+ return {
+ element: `.frappe-control[data-fieldname='${step.fieldname}']`,
+ popover: {
+ title: step.title || field.label,
+ description: step.description
+ }
+ };
+ });
+
+ driver.defineSteps(steps);
+ driver.start();
+ }
};
frappe.validated = 0;
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index d178c59100..9f4a2a61d6 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -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) {
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 5aeb29b1ed..d6106255a0 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -599,12 +599,15 @@ frappe.ui.form.Section = Class.extend({
if(this.df.cssClass) {
this.wrapper.addClass(this.df.cssClass);
}
+ if (this.df.hide_border) {
+ this.wrapper.toggleClass("hide-border", true);
+ }
}
-
// for bc
this.body = $('').appendTo(this.wrapper);
},
+
make_head: function() {
var me = this;
if(!this.df.collapsible) {
@@ -663,9 +666,11 @@ frappe.ui.form.Section = Class.extend({
}
});
},
+
is_collapsed() {
return this.body.hasClass('hide');
},
+
has_missing_mandatory: function() {
var missing_mandatory = false;
for (var j=0, l=this.fields_list.length; j < l; j++) {
diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js
index cdd385a6ea..41b87e0207 100644
--- a/frappe/public/js/frappe/form/multi_select_dialog.js
+++ b/frappe/public/js/frappe/form/multi_select_dialog.js
@@ -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(`
`);
@@ -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 += `
${
- head ? `
${__(frappe.model.unscrub(column))} `
-
- : (column !== "name" ? `
${__(result[column])} `
- : `
- ${__(result[column])} `)
- }
+ head ? `
${__(frappe.model.unscrub(column))} `
+ : (column !== "name" ? `
${__(result[column] || '')} `
+ : `
+ ${__(result[column] || '')} `)}
`;
- })
+ });
let $row = $(`
@@ -215,10 +238,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
head ? $row.addClass('list-item--head')
: $row = $(`
`).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);
}
});
- },
-
-});
\ No newline at end of file
+ }
+};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js
index 9996389a4e..68444c8a3b 100644
--- a/frappe/public/js/frappe/form/quick_entry.js
+++ b/frappe/public/js/frappe/form/quick_entry.js
@@ -107,7 +107,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
});
this.register_primary_action();
- this.render_edit_in_full_page_link();
+ !this.force && this.render_edit_in_full_page_link();
// ctrl+enter to save
this.dialog.wrapper.keydown(function(e) {
if((e.ctrlKey || e.metaKey) && e.which==13) {
@@ -213,8 +213,15 @@ frappe.ui.form.QuickEntryForm = Class.extend({
me.dialog.doc = r.message;
if (frappe._from_link) {
frappe.ui.form.update_calling_link(me.dialog.doc);
+ } else {
+ if (me.after_insert) {
+ me.after_insert(me.dialog.doc);
+ } else {
+ me.open_form_if_not_list();
+ }
}
- cur_frm.reload_doc();
+
+ cur_frm && cur_frm.reload_doc();
}
});
},
diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js
index fd5b8d3856..1238bf141c 100644
--- a/frappe/public/js/frappe/form/script_manager.js
+++ b/frappe/public/js/frappe/form/script_manager.js
@@ -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;
}
}
diff --git a/frappe/public/js/frappe/form/sidebar/assign_to.js b/frappe/public/js/frappe/form/sidebar/assign_to.js
index 61d1789518..95ceb246e6 100644
--- a/frappe/public/js/frappe/form/sidebar/assign_to.js
+++ b/frappe/public/js/frappe/form/sidebar/assign_to.js
@@ -87,23 +87,17 @@ frappe.ui.form.AssignTo = Class.extend({
if(!me.assign_to) {
me.assign_to = new frappe.ui.form.AssignToDialog({
- obj: me,
- method: 'frappe.desk.form.assign_to.add',
+ method: "frappe.desk.form.assign_to.add",
doctype: me.frm.doctype,
docname: me.frm.docname,
- callback: function(r) {
+ frm: me.frm,
+ callback: function (r) {
me.render(r.message);
}
});
}
me.assign_to.dialog.clear();
-
- if(me.frm.meta.title_field) {
- me.assign_to.dialog.set_value("description", me.frm.doc[me.frm.meta.title_field])
- }
-
me.assign_to.dialog.show();
- me.assign_to = null;
},
remove: function(owner) {
var me = this;
@@ -130,81 +124,126 @@ frappe.ui.form.AssignTo = Class.extend({
frappe.ui.form.AssignToDialog = Class.extend({
init: function(opts){
- var me = this
- var dialog = new frappe.ui.Dialog({
- title: __('Add to To Do'),
- fields: [
- { fieldtype: 'Link', fieldname: 'assign_to', options: 'User', label: __("Assign To"), reqd: true, filters: { 'user_type': 'System User' }},
- { fieldtype: 'Check', fieldname: 'myself', label: __("Assign to me"), "default": 0 },
- { fieldtype: 'Small Text', fieldname: 'description', label: __("Comment") },
- { fieldtype: 'Section Break' },
- { fieldtype: 'Column Break' },
- { fieldtype: 'Date', fieldname: 'date', label: __("Complete By") },
- { fieldtype: 'Column Break' },
- { fieldtype: 'Select', fieldname: 'priority', label: __("Priority"),
- options: [
- { value: 'Low', label: __('Low') },
- { value: 'Medium', label: __('Medium') },
- { value: 'High', label: __('High') }
- ],
- // Pick up priority from the source document, if it exists and is available in ToDo
- 'default': ["Low", "Medium", "High"].includes(opts.obj.frm && opts.obj.frm.doc.priority
- ? opts.obj.frm.doc.priority : 'Medium')
- },
- ],
- primary_action: function() { frappe.ui.add_assignment(opts, this) },
- primary_action_label: __("Add")
- })
- $.extend(me, dialog);
+ $.extend(this, opts);
- me.dialog = dialog;
-
- me.dialog.fields_dict.assign_to.get_query = "frappe.core.doctype.user.user.user_query";
-
- var myself = me.dialog.get_input("myself").on("click", function() {
- me.toggle_myself(this);
- });
- me.toggle_myself(myself);
- },
- toggle_myself: function(myself) {
- var me = this;
- if($(myself).prop("checked")) {
- me.dialog.set_value("assign_to", frappe.session.user);
- me.dialog.get_field("notify").$wrapper.toggle(false);
- me.dialog.get_field("assign_to").$wrapper.toggle(false);
- } else {
- me.dialog.set_value("assign_to", "");
- me.dialog.get_field("assign_to").$wrapper.toggle(true);
- }
+ this.make();
+ this.set_description_from_doc();
},
+ make: function() {
+ let me = this;
-});
+ me.dialog = new frappe.ui.Dialog({
+ title: __('Add to ToDo'),
+ fields: me.get_fields(),
+ primary_action_label: __("Add"),
+ primary_action: function() {
+ let args = me.dialog.get_values();
-frappe.ui.add_assignment = function(opts, dialog) {
- var assign_to = dialog.fields_dict.assign_to.get_value();
- var args = dialog.get_values();
- if(args && assign_to) {
- dialog.set_message('Assigning...');
- return frappe.call({
- method: opts.method,
- args: $.extend(args, {
- doctype: opts.doctype,
- name: opts.docname,
- assign_to: assign_to,
- bulk_assign: opts.bulk_assign || false,
- re_assign: opts.re_assign || false
- }),
- btn: dialog.get_primary_btn(),
- callback: function(r) {
- if(!r.exc) {
- if(opts.callback){
- opts.callback(r);
- }
- dialog && dialog.hide();
- } else {
- dialog.clear_message();
+ if (args && args.assign_to) {
+ me.dialog.set_message("Assigning...");
+
+ frappe.call({
+ method: me.method,
+ args: $.extend(args, {
+ doctype: me.doctype,
+ name: me.docname,
+ assign_to: args.assign_to,
+ bulk_assign: me.bulk_assign || false,
+ re_assign: me.re_assign || false
+ }),
+ btn: me.dialog.get_primary_btn(),
+ callback: function(r) {
+ if (!r.exc) {
+ if (me.callback) {
+ me.callback(r);
+ }
+ me.dialog && me.dialog.hide();
+ } else {
+ me.dialog.clear_message();
+ }
+ },
+ });
}
},
});
+ },
+ assign_to_me: function() {
+ let me = this;
+ let assign_to = [];
+
+ if (me.dialog.get_value("assign_to_me")) {
+ assign_to.push(frappe.session.user);
+ }
+
+ me.dialog.set_value("assign_to", assign_to);
+ },
+ set_description_from_doc: function() {
+ let me = this;
+
+ if (me.frm && me.frm.meta.title_field) {
+ me.dialog.set_value("description", me.frm.doc[me.frm.meta.title_field]);
+ }
+ },
+ get_fields: function() {
+ let me = this;
+
+ return [
+ {
+ fieldtype: 'MultiSelectPills',
+ fieldname: 'assign_to',
+ label: __("Assign To"),
+ reqd: true,
+ get_data: function(txt) {
+ return frappe.db.get_link_options("User", txt, {user_type: "System User", enabled: 1});
+ }
+ },
+ {
+ label: __("Assign to me"),
+ fieldtype: 'Check',
+ fieldname: 'assign_to_me',
+ default: 0,
+ onchange: () => me.assign_to_me()
+ },
+ {
+ label: __("Comment"),
+ fieldtype: 'Small Text',
+ fieldname: 'description'
+ },
+ {
+ fieldtype: 'Section Break'
+ },
+ {
+ fieldtype: 'Column Break'
+ },
+ {
+ label: __("Complete By"),
+ fieldtype: 'Date',
+ fieldname: 'date'
+ },
+ {
+ fieldtype: 'Column Break'
+ },
+ {
+ label: __("Priority"),
+ fieldtype: 'Select',
+ fieldname: 'priority',
+ options: [
+ {
+ value: 'Low',
+ label: __('Low')
+ },
+ {
+ value: 'Medium',
+ label: __('Medium')
+ },
+ {
+ value: 'High',
+ label: __('High')
+ }
+ ],
+ // Pick up priority from the source document, if it exists and is available in ToDo
+ default: ["Low", "Medium", "High"].includes(me.frm && me.frm.doc.priority ? me.frm.doc.priority : 'Medium')
+ }
+ ];
}
-}
+});
diff --git a/frappe/public/js/frappe/form/sidebar/review.js b/frappe/public/js/frappe/form/sidebar/review.js
index e187ca4693..2cf2980bf7 100644
--- a/frappe/public/js/frappe/form/sidebar/review.js
+++ b/frappe/public/js/frappe/form/sidebar/review.js
@@ -21,6 +21,12 @@ frappe.ui.form.Review = class Review {
});
}
make_review_container() {
+ this.parent.append(`
+
+ `);
this.review_list_wrapper = this.parent.find('.review-list');
}
add_review_button() {
diff --git a/frappe/public/js/frappe/form/templates/address_list.html b/frappe/public/js/frappe/form/templates/address_list.html
new file mode 100644
index 0000000000..0f967b67a0
--- /dev/null
+++ b/frappe/public/js/frappe/form/templates/address_list.html
@@ -0,0 +1,22 @@
+
+{% for(var i=0, l=addr_list.length; i
+
+ {%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
+ ({%= __(addr_list[i].address_type) %}) {% } %}
+ {% if(addr_list[i].is_primary_address) { %}
+ ({%= __("Primary") %}) {% } %}
+ {% if(addr_list[i].is_shipping_address) { %}
+ ({%= __("Shipping") %}) {% } %}
+
+
+ {%= __("Edit") %}
+
+ {%= addr_list[i].display %}
+
+{% } %}
+{% if(!addr_list.length) { %}
+
{%= __("No address added yet.") %}
+{% } %}
+
{{ __("New Address") }}
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/templates/contact_list.html b/frappe/public/js/frappe/form/templates/contact_list.html
new file mode 100644
index 0000000000..7e6969163b
--- /dev/null
+++ b/frappe/public/js/frappe/form/templates/contact_list.html
@@ -0,0 +1,54 @@
+
+{% for(var i=0, l=contact_list.length; i
+
+ {%= contact_list[i].first_name %} {%= contact_list[i].last_name %}
+ {% if(contact_list[i].is_primary_contact) { %}
+ ({%= __("Primary") %})
+ {% } %}
+ {% if(contact_list[i].designation){ %}
+ – {%= contact_list[i].designation %}
+ {% } %}
+
+ {%= __("Edit") %}
+
+ {% if (contact_list[i].phones || contact_list[i].email_ids) { %}
+
+ {% if(contact_list[i].phone) { %}
+ {%= __("Phone") %}: {%= contact_list[i].phone %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].mobile_no) { %}
+ {%= __("Mobile No") %}: {%= contact_list[i].mobile_no %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].phone_nos) { %}
+ {% for(var j=0, k=contact_list[i].phone_nos.length; j
+ {% } %}
+ {% endif %}
+
+
+ {% if(contact_list[i].email_id) { %}
+ {%= __("Email") %}: {%= contact_list[i].email_id %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].email_ids) { %}
+ {% for(var j=0, k=contact_list[i].email_ids.length; j
+ {% } %}
+ {% endif %}
+
+ {% endif %}
+
+ {% if (contact_list[i].address) { %}
+ {%= __("Address") %}: {%= contact_list[i].address %}
+ {% endif %}
+
+
+{% } %}
+{% if(!contact_list.length) { %}
+
{%= __("No contacts added yet.") %}
+{% } %}
+
+ {{ __("New Contact") }}
+
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html
index 30b2205bae..c3f2de9c7e 100644
--- a/frappe/public/js/frappe/form/templates/form_sidebar.html
+++ b/frappe/public/js/frappe/form/templates/form_sidebar.html
@@ -69,10 +69,7 @@
-
+
-
@@ -234,8 +216,8 @@ export default {
{label: "Exact Copies", slug: "exact_copies", sortable: true},
],
query: {
- sort: "index",
- order: "asc",
+ sort: "duration",
+ order: "desc",
pagination: {
limit: 20,
page: 1,
@@ -244,7 +226,6 @@ export default {
},
group_duplicates: false,
showing: null,
- showing_traceback: null,
request: {
calls: [],
},
diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js
index 06bd6a3bd9..f3f3285245 100644
--- a/frappe/public/js/frappe/router.js
+++ b/frappe/public/js/frappe/router.js
@@ -12,6 +12,7 @@ frappe.route_history = [];
frappe.view_factory = {};
frappe.view_factories = [];
frappe.route_options = null;
+frappe.route_hooks = {};
frappe.route = function() {
diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js
index 9983a35779..1411b6289d 100644
--- a/frappe/public/js/frappe/socketio_client.js
+++ b/frappe/public/js/frappe/socketio_client.js
@@ -287,7 +287,8 @@ frappe.socketio.SocketIOUploader = class SocketIOUploader {
}
function fallback_required() {
- return !frappe.boot.sysdefaults.use_socketio_to_upload_file || !frappe.socketio.socket.connected;
+ return !frappe.socketio.socket.connected
+ || !( !frappe.boot.sysdefaults || frappe.boot.sysdefaults.use_socketio_to_upload_file );
}
if (fallback_required()) {
diff --git a/frappe/public/js/frappe/ui/driver.js b/frappe/public/js/frappe/ui/driver.js
new file mode 100644
index 0000000000..98ed49ec05
--- /dev/null
+++ b/frappe/public/js/frappe/ui/driver.js
@@ -0,0 +1,3 @@
+import Driver from 'driver.js';
+
+frappe.Driver = Driver;
\ No newline at end of file
diff --git a/frappe/public/js/frappe/ui/filters/field_select.js b/frappe/public/js/frappe/ui/filters/field_select.js
index 672991a554..65d32184e3 100644
--- a/frappe/public/js/frappe/ui/filters/field_select.js
+++ b/frappe/public/js/frappe/ui/filters/field_select.js
@@ -119,7 +119,14 @@ frappe.ui.FieldSelect = Class.extend({
// child tables
$.each(me.table_fields, function(i, table_df) {
if(table_df.options) {
- var child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]);
+ let child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]);
+
+ if (table_df.fieldtype === "Table MultiSelect") {
+ const link_field = frappe.meta.get_docfields(table_df.options)
+ .find(df => df.fieldtype === 'Link');
+ child_table_fields = link_field ? [link_field] : [];
+ }
+
$.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) {
// show fields where user has read access and if report hide flag is not set
if(frappe.perm.has_perm(me.doctype, df.permlevel, "read"))
@@ -130,15 +137,22 @@ frappe.ui.FieldSelect = Class.extend({
},
add_field_option(df) {
- if (df.fieldname == 'docstatus' && !frappe.model.is_submittable(this.doctype))
+ let me = this;
+
+ if (df.fieldname == 'docstatus' && !frappe.model.is_submittable(me.doctype))
return;
- var me = this;
- var label, table;
+ if (frappe.model.table_fields.includes(df.fieldtype)) {
+ me.table_fields.push(df);
+ return;
+ }
+
+ let label = null;
+ let table = null;
+
if(me.doctype && df.parent==me.doctype) {
label = __(df.label);
table = me.doctype;
- if(frappe.model.table_fields.includes(df.fieldtype)) me.table_fields.push(df);
} else {
label = __(df.label) + ' (' + __(df.parent) + ')';
table = df.parent;
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index 818612d442..37eab50957 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -6,6 +6,12 @@ frappe.ui.Filter = class {
}
this.utils = frappe.ui.filter_utils;
+ this.set_conditions();
+ this.set_conditions_from_config();
+ this.make();
+ }
+
+ set_conditions() {
this.conditions = [
["=", __("Equals")],
["!=", __("Not Equals")],
@@ -19,8 +25,7 @@ frappe.ui.Filter = class {
[">=", ">="],
["<=", "<="],
["Between", __("Between")],
- ["Previous", __("Previous")],
- ["Next", __("Next")]
+ ["Timespan", __("Timespan")],
];
this.nested_set_conditions = [
@@ -35,17 +40,28 @@ frappe.ui.Filter = class {
this.invalid_condition_map = {
Date: ['like', 'not like'],
Datetime: ['like', 'not like'],
- Data: ['Between', 'Previous', 'Next'],
- Select: ['like', 'not like', 'Between', 'Previous', 'Next'],
- Link: ["Between", 'Previous', 'Next', '>', '<', '>=', '<='],
- Currency: ["Between", 'Previous', 'Next'],
- Color: ["Between", 'Previous', 'Next'],
+ Data: ['Between', 'Timespan'],
+ Select: ['like', 'not like', 'Between', 'Timespan'],
+ Link: ["Between", 'Timespan', '>', '<', '>=', '<='],
+ Currency: ["Between", 'Timespan'],
+ Color: ["Between", 'Timespan'],
Check: this.conditions.map(c => c[0]).filter(c => c !== '=')
};
- this.make();
- this.make_select();
- this.set_events();
- this.setup();
+ }
+
+ set_conditions_from_config() {
+ if (frappe.boot.additional_filters_config) {
+ this.filters_config = frappe.boot.additional_filters_config;
+ for (let key of Object.keys(this.filters_config)) {
+ const filter = this.filters_config[key];
+ this.conditions.push([key, __(`{0}`, [filter.label])]);
+ for (let fieldtype of Object.keys(this.invalid_condition_map)) {
+ if (!filter.valid_for_fieldtypes.includes(fieldtype)) {
+ this.invalid_condition_map[fieldtype].push(filter.label);
+ }
+ }
+ }
+ }
}
make() {
@@ -53,6 +69,10 @@ frappe.ui.Filter = class {
conditions: this.conditions
}))
.appendTo(this.parent.find('.filter-edit-area'));
+
+ this.make_select();
+ this.set_events();
+ this.setup();
}
make_select() {
@@ -203,33 +223,23 @@ frappe.ui.Filter = class {
this.fieldselect.selected_doctype = doctype;
this.fieldselect.selected_fieldname = fieldname;
- if(["Previous", "Next"].includes(condition) && ['Date', 'Datetime', 'DateRange', 'Select'].includes(this.field.df.fieldtype)) {
- df.fieldtype = 'Select';
- df.options = [
- {
- label: __('1 week'),
- value: '1 week'
- },
- {
- label: __('1 month'),
- value: '1 month'
- },
- {
- label: __('3 months'),
- value: '3 months'
- },
- {
- label: __('6 months'),
- value: '6 months'
- },
- {
- label: __('1 year'),
- value: '1 year'
- }
- ];
+ if (this.filters_config && this.filters_config[condition]
+ && this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)) {
+ let args = {};
+ if (this.filters_config[condition].depends_on) {
+ const field_name = this.filters_config[condition].depends_on;
+ const filter_value = this.base_list.get_filter_value(field_name);
+ args[field_name] = filter_value;
+ }
+ frappe.xcall(this.filters_config[condition].get_field, args).then(field => {
+ df.fieldtype = field.fieldtype;
+ df.options = field.options;
+ df.fieldname = fieldname;
+ this.make_field(df, cur.fieldtype);
+ });
+ } else {
+ this.make_field(df, cur.fieldtype);
}
-
- this.make_field(df, cur.fieldtype);
}
make_field(df, old_fieldtype) {
@@ -440,6 +450,10 @@ frappe.ui.filter_utils = {
if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){
df.fieldtype = 'DateRange';
}
+ if (condition == 'Timespan' && ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)) {
+ df.fieldtype = 'Select';
+ df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']);
+ }
if (condition === 'is') {
df.fieldtype = 'Select';
df.options = [
@@ -447,5 +461,32 @@ frappe.ui.filter_utils = {
{ label: __('Not Set'), value: 'not set' },
];
}
+ return;
+ },
+
+ get_timespan_options(periods) {
+ const period_map = {
+ 'Last': ['Week', 'Month', 'Quarter', '6 months', 'Year'],
+ 'Today': null,
+ 'This': ['Week', 'Month', 'Quarter', 'Year'],
+ 'Next': ['Week', 'Month', 'Quarter', '6 months', 'Year']
+ };
+ let options = [];
+ periods.forEach(period => {
+ if (period_map[period]) {
+ period_map[period].forEach(p => {
+ options.push({
+ label: __(`{0} {1}`, [period, p]),
+ value: `${period.toLowerCase()} ${p.toLowerCase()}`,
+ });
+ });
+ } else {
+ options.push({
+ label: __(`{0}`, [period]),
+ value: `${period.toLowerCase()}`,
+ });
+ }
+ });
+ return options;
}
};
diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js
index db6398ca78..ed9ddefe64 100644
--- a/frappe/public/js/frappe/ui/filters/filter_list.js
+++ b/frappe/public/js/frappe/ui/filters/filter_list.js
@@ -103,7 +103,8 @@ frappe.ui.FilterGroup = class {
},
filter_items: (doctype, fieldname) => {
return !this.filter_exists([doctype, fieldname]);
- }
+ },
+ base_list: this.base_list
};
let filter = new frappe.ui.Filter(args);
this.filters.push(filter);
@@ -132,7 +133,6 @@ frappe.ui.FilterGroup = class {
get_filters() {
return this.filters.filter(f => f.field).map(f => {
- f.freeze();
return f.get_value();
});
// {}: this.list.update_standard_filters(values);
diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js
index 3646dc6b6e..f8f0535b83 100644
--- a/frappe/public/js/frappe/ui/filters/filters.js
+++ b/frappe/public/js/frappe/ui/filters/filters.js
@@ -200,6 +200,12 @@ frappe.ui.FilterList = Class.extend({
value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
} else if(field.df.original_type==="Check") {
value = {0:"No", 1:"Yes"}[cint(value)];
+ } else if (field.df.original_type === "Duration") {
+ let duration_options = {
+ show_days: field.df.show_days,
+ show_seconds: field.df.show_seconds
+ };
+ value = frappe.utils.get_formatted_duration(value, duration_options);
}
value = frappe.format(value, field.df, {only_value: 1});
diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js
index 831bafa8e8..5053ad71f4 100644
--- a/frappe/public/js/frappe/ui/group_by/group_by.js
+++ b/frappe/public/js/frappe/ui/group_by/group_by.js
@@ -241,6 +241,7 @@ frappe.ui.GroupBy = class {
this.order_by = '';
this.group_by = null;
+ this.report_view.group_by = null;
this.aggregate_function = null;
this.aggregate_on = null;
$(".groupby").val("");
diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js
index 2420d6772e..3570420c81 100644
--- a/frappe/public/js/frappe/ui/notifications/notifications.js
+++ b/frappe/public/js/frappe/ui/notifications/notifications.js
@@ -153,12 +153,12 @@ frappe.ui.Notifications = class Notifications {
let title = target ? `title="${__('Your Target')}"` : '';
let $list_item = !target
? $(`
- ${label}
+ ${__(label)}
${value}
`)
: $(`
- ${label}
+ ${__(label)}
@@ -304,10 +304,7 @@ frappe.ui.Notifications = class Notifications {
}
get_dropdown_item_html(field) {
- let doc_link = frappe.utils.get_form_link(
- field.document_type,
- field.document_name
- );
+ let doc_link = this.get_item_link(field);
let read_class = field.read ? '' : 'unread';
let mark_read_action = field.read ? '': 'data-action="mark_as_read"';
let message = field.subject;
@@ -336,6 +333,17 @@ frappe.ui.Notifications = class Notifications {
return item_html;
}
+ get_item_link(notification_doc) {
+ const link_doctype =
+ notification_doc.type == 'Alert' ? 'Notification Log': notification_doc.document_type;
+ const link_docname =
+ notification_doc.type == 'Alert' ? notification_doc.name: notification_doc.document_name;
+ return frappe.utils.get_form_link(
+ link_doctype,
+ link_docname
+ );
+ }
+
render_dropdown_headers() {
this.categories = [
{
diff --git a/frappe/public/js/frappe/ui/toolbar/about.js b/frappe/public/js/frappe/ui/toolbar/about.js
index 13ba4836fb..87462916c2 100644
--- a/frappe/public/js/frappe/ui/toolbar/about.js
+++ b/frappe/public/js/frappe/ui/toolbar/about.js
@@ -9,6 +9,12 @@ frappe.ui.misc.about = function() {
Website:
https://frappe.io \
\
Source: https://github.com/frappe
\
+
\
+ Linkedin: https://linkedin.com/company/frappe-tech
\
+
\
+ Facebook: https://facebook.com/erpnext
\
+
\
+ Twitter: https://twitter.com/erpnext
\
\
Installed Apps \
Loading versions...
\
diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
index 0595fb6219..3e59986928 100644
--- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
+++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
@@ -177,6 +177,7 @@ frappe.search.AwesomeBar = Class.extend({
frappe.search.utils.get_reports(txt),
frappe.search.utils.get_pages(txt),
frappe.search.utils.get_workspaces(txt),
+ frappe.search.utils.get_dashboards(txt),
frappe.search.utils.get_recent_pages(txt || ""),
frappe.search.utils.get_executables(txt)
);
diff --git a/frappe/public/js/frappe/ui/toolbar/search_utils.js b/frappe/public/js/frappe/ui/toolbar/search_utils.js
index 9a03360d59..6161368766 100644
--- a/frappe/public/js/frappe/ui/toolbar/search_utils.js
+++ b/frappe/public/js/frappe/ui/toolbar/search_utils.js
@@ -288,7 +288,7 @@ frappe.search.utils = {
var out = [];
frappe.boot.allowed_workspaces.forEach(function(item) {
var level = me.fuzzy_search(keywords, item.name);
- if(level > 0) {
+ if (level > 0) {
var ret = {
type: "Workspace",
label: __("Open {0}", [me.bolden_match_part(__(item.name), keywords)]),
@@ -303,6 +303,26 @@ frappe.search.utils = {
return out;
},
+ get_dashboards: function(keywords) {
+ var me = this;
+ var out = [];
+ frappe.boot.dashboards.forEach(function(item) {
+ var level = me.fuzzy_search(keywords, item.name);
+ if (level > 0) {
+ var ret = {
+ type: "Dashboard",
+ label: __("{0} Dashboard", [me.bolden_match_part(__(item.name), keywords)]),
+ value: __("{0} Dashboard", [__(item.name)]),
+ index: level,
+ route: ["dashboard", item.name]
+ };
+
+ out.push(ret);
+ }
+ });
+ return out;
+ },
+
get_global_results: function(keywords, start, limit, doctype = "") {
var me = this;
function get_results_sets(data) {
@@ -490,6 +510,11 @@ frappe.search.utils = {
fetch_type: "Nav",
results: sort_uniques(this.get_workspaces(keywords))
},
+ {
+ title: "Dashboard",
+ fetch_type: "Nav",
+ results: sort_uniques(this.get_dashboards(keywords))
+ },
{
title: "Setup",
fetch_type: "Nav",
diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js
index 1cdabf23e0..9ff4ade761 100644
--- a/frappe/public/js/frappe/utils/common.js
+++ b/frappe/public/js/frappe/utils/common.js
@@ -276,7 +276,7 @@ frappe.utils.sanitise_redirect = (url) => {
// check for base domain only if the url is absolute
// return true for relative url (except protocol-relative urls)
- return is_absolute(url) ? domain(location.href) !== domain(url) : true;
+ return is_absolute(url) ? domain(location.href) !== domain(url) : false;
}
})();
diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js
index a1628be34a..d1621a3e15 100644
--- a/frappe/public/js/frappe/utils/dashboard_utils.js
+++ b/frappe/public/js/frappe/utils/dashboard_utils.js
@@ -82,5 +82,21 @@ frappe.dashboard_utils = {
).then(settings => {
return settings;
});
+ },
+
+ get_years_since_creation(creation) {
+ //Get years since user account created
+ let creation_year = this.get_year(creation);
+ let current_year = this.get_year(frappe.datetime.now_date());
+ let years_list = [];
+ for (var year = current_year; year >= creation_year; year--) {
+ years_list.push(year);
+ }
+ return years_list;
+ },
+
+ get_year(date_str) {
+ return date_str.substring(0, date_str.indexOf('-'));
}
+
};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/utils/pretty_date.js b/frappe/public/js/frappe/utils/pretty_date.js
index ef235ed3b1..7618d58829 100644
--- a/frappe/public/js/frappe/utils/pretty_date.js
+++ b/frappe/public/js/frappe/utils/pretty_date.js
@@ -76,6 +76,7 @@ window.comment_when = function(datetime, mini) {
+ prettyDate(datetime, mini) + '';
};
frappe.datetime.comment_when = comment_when;
+frappe.datetime.prettyDate = prettyDate;
frappe.datetime.refresh_when = function() {
if (jQuery) {
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 7eff0b8e24..24fa946fc4 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -250,7 +250,8 @@ Object.assign(frappe.utils, {
regExp = /^\w+$/;
break;
case "email":
- regExp = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
+ // from https://emailregex.com/
+ regExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
break;
case "url":
regExp = /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
@@ -802,6 +803,70 @@ Object.assign(frappe.utils, {
name: M[0],
version: M[1],
};
+ },
+
+ get_formatted_duration(value, duration_options) {
+ let duration = '';
+ if (value) {
+ let total_duration = frappe.utils.seconds_to_duration(value, duration_options);
+
+ if (total_duration.days) {
+ duration += total_duration.days + __('d', null, 'Days (Field: Duration)');
+ }
+ if (total_duration.hours) {
+ duration += (duration.length ? " " : "");
+ duration += total_duration.hours + __('h', null, 'Hours (Field: Duration)');
+ }
+ if (total_duration.minutes) {
+ duration += (duration.length ? " " : "");
+ duration += total_duration.minutes + __('m', null, 'Minutes (Field: Duration)');
+ }
+ if (total_duration.seconds) {
+ duration += (duration.length ? " " : "");
+ duration += total_duration.seconds + __('s', null, 'Seconds (Field: Duration)');
+ }
+ }
+ return duration;
+ },
+
+ seconds_to_duration(value, duration_options) {
+ let secs = value;
+ let total_duration = {
+ days: Math.floor(secs / (3600 * 24)),
+ hours: Math.floor(secs % (3600 * 24) / 3600),
+ minutes: Math.floor(secs % 3600 / 60),
+ seconds: Math.floor(secs % 60)
+ };
+ if (!duration_options.show_days) {
+ total_duration.hours = Math.floor(secs / 3600);
+ total_duration.days = 0;
+ }
+ return total_duration;
+ },
+
+ duration_to_seconds(days=0, hours=0, minutes=0, seconds=0) {
+ let value = 0;
+ if (days) {
+ value += days * 24 * 60 * 60;
+ }
+ if (hours) {
+ value += hours * 60 * 60;
+ }
+ if (minutes) {
+ value += minutes * 60;
+ }
+ if (seconds) {
+ value += seconds;
+ }
+ return value;
+ },
+
+ get_duration_options: function(docfield) {
+ let duration_options = {
+ show_days: docfield.show_days,
+ show_seconds: docfield.show_seconds
+ };
+ return duration_options;
}
});
@@ -822,3 +887,115 @@ if (!Array.prototype.uniqBy) {
}
});
}
+
+// Pluralize
+String.prototype.plural = function(revert) {
+ const plural = {
+ "(quiz)$": "$1zes",
+ "^(ox)$": "$1en",
+ "([m|l])ouse$": "$1ice",
+ "(matr|vert|ind)ix|ex$": "$1ices",
+ "(x|ch|ss|sh)$": "$1es",
+ "([^aeiouy]|qu)y$": "$1ies",
+ "(hive)$": "$1s",
+ "(?:([^f])fe|([lr])f)$": "$1$2ves",
+ "(shea|lea|loa|thie)f$": "$1ves",
+ sis$: "ses",
+ "([ti])um$": "$1a",
+ "(tomat|potat|ech|her|vet)o$": "$1oes",
+ "(bu)s$": "$1ses",
+ "(alias)$": "$1es",
+ "(octop)us$": "$1i",
+ "(ax|test)is$": "$1es",
+ "(us)$": "$1es",
+ "([^s]+)$": "$1s",
+ };
+
+ const singular = {
+ "(quiz)zes$": "$1",
+ "(matr)ices$": "$1ix",
+ "(vert|ind)ices$": "$1ex",
+ "^(ox)en$": "$1",
+ "(alias)es$": "$1",
+ "(octop|vir)i$": "$1us",
+ "(cris|ax|test)es$": "$1is",
+ "(shoe)s$": "$1",
+ "(o)es$": "$1",
+ "(bus)es$": "$1",
+ "([m|l])ice$": "$1ouse",
+ "(x|ch|ss|sh)es$": "$1",
+ "(m)ovies$": "$1ovie",
+ "(s)eries$": "$1eries",
+ "([^aeiouy]|qu)ies$": "$1y",
+ "([lr])ves$": "$1f",
+ "(tive)s$": "$1",
+ "(hive)s$": "$1",
+ "(li|wi|kni)ves$": "$1fe",
+ "(shea|loa|lea|thie)ves$": "$1f",
+ "(^analy)ses$": "$1sis",
+ "((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$":
+ "$1$2sis",
+ "([ti])a$": "$1um",
+ "(n)ews$": "$1ews",
+ "(h|bl)ouses$": "$1ouse",
+ "(corpse)s$": "$1",
+ "(us)es$": "$1",
+ s$: "",
+ };
+
+ const irregular = {
+ move: "moves",
+ foot: "feet",
+ goose: "geese",
+ sex: "sexes",
+ child: "children",
+ man: "men",
+ tooth: "teeth",
+ person: "people",
+ };
+
+ const uncountable = [
+ "sheep",
+ "fish",
+ "deer",
+ "moose",
+ "series",
+ "species",
+ "money",
+ "rice",
+ "information",
+ "equipment",
+ ];
+
+ // save some time in the case that singular and plural are the same
+ if (uncountable.indexOf(this.toLowerCase()) >= 0) return this;
+
+ // check for irregular forms
+ let word;
+ let pattern;
+ let replace;
+ for (word in irregular) {
+ if (revert) {
+ pattern = new RegExp(irregular[word] + "$", "i");
+ replace = word;
+ } else {
+ pattern = new RegExp(word + "$", "i");
+ replace = irregular[word];
+ }
+ if (pattern.test(this)) return this.replace(pattern, replace);
+ }
+
+ let array;
+ if (revert) array = singular;
+ else array = plural;
+
+ // check for matches using regular expressions
+ let reg;
+ for (reg in array) {
+ pattern = new RegExp(reg, "i");
+
+ if (pattern.test(this)) return this.replace(pattern, array[reg]);
+ }
+
+ return this;
+};
diff --git a/frappe/public/js/frappe/utils/web_page_block.js b/frappe/public/js/frappe/utils/web_page_block.js
deleted file mode 100644
index bcf821bcfa..0000000000
--- a/frappe/public/js/frappe/utils/web_page_block.js
+++ /dev/null
@@ -1,23 +0,0 @@
-frappe.ui.form.on('Web Page Block', {
- edit_values(frm, cdt, cdn) {
- let row = frm.selected_doc;
- frappe.model.with_doc('Web Template', row.web_template).then(doc => {
- let d = new frappe.ui.Dialog({
- title: __('Edit Values'),
- fields: doc.fields,
- primary_action(values) {
- frappe.model.set_value(
- cdt,
- cdn,
- 'web_template_values',
- JSON.stringify(values)
- );
- d.hide();
- }
- });
- let values = JSON.parse(row.web_template_values || '{}');
- d.set_values(values);
- d.show();
- });
- }
-});
diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js
index 1c1049391f..0058310e3f 100644
--- a/frappe/public/js/frappe/views/breadcrumbs.js
+++ b/frappe/public/js/frappe/views/breadcrumbs.js
@@ -89,16 +89,21 @@ frappe.breadcrumbs = {
breadcrumbs.module = frappe.breadcrumbs.module_map[breadcrumbs.module];
}
- if(frappe.get_module(breadcrumbs.module)) {
+ let current_module = breadcrumbs.module
+ // Check if a desk page exists
+ if (frappe.boot.module_page_map[breadcrumbs.module]) {
+ breadcrumbs.module = frappe.boot.module_page_map[breadcrumbs.module];
+ }
+
+ if(frappe.get_module(current_module)) {
// if module access exists
- var module_info = frappe.get_module(breadcrumbs.module),
+ var module_info = frappe.get_module(current_module),
icon = module_info && module_info.icon,
label = module_info ? module_info.label : breadcrumbs.module;
-
if(module_info && !module_info.blocked && frappe.visible_modules.includes(module_info.module_name)) {
$(repl('
%(label)s ',
- { module: breadcrumbs.module, label: __(label) }))
+ { module: breadcrumbs.module, label: __(breadcrumbs.module) }))
.appendTo($breadcrumbs);
}
}
diff --git a/frappe/public/js/frappe/views/dashboard/dashboard_view.js b/frappe/public/js/frappe/views/dashboard/dashboard_view.js
index 13c44d2130..83f45da5be 100644
--- a/frappe/public/js/frappe/views/dashboard/dashboard_view.js
+++ b/frappe/public/js/frappe/views/dashboard/dashboard_view.js
@@ -41,7 +41,13 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
this.$dashboard_page = this.$page.find('.layout-main-section-wrapper').addClass('dashboard-page');
this.$page.find('.page-form').empty().html(
`